forked from Minki/linux
16427faf28
On systems where it cannot be assured that enough continous memory is available all the time it can be very useful to only allocate the memory once when it is needed the first time. Afterwards the initially allocated memory will be reused, so it is ensured that the memory will stay available until the driver is unloaded. [mchehab@redhat.com: Codingstyle fixups] Signed-off-by: Julian Scheel <julian@jusst.de> Signed-off-by: Mauro Carvalho Chehab <mchehab@redhat.com>
1928 lines
46 KiB
C
1928 lines
46 KiB
C
/*
|
|
* tm6000-video.c - driver for TM5600/TM6000/TM6010 USB video capture devices
|
|
*
|
|
* Copyright (C) 2006-2007 Mauro Carvalho Chehab <mchehab@infradead.org>
|
|
*
|
|
* Copyright (C) 2007 Michel Ludwig <michel.ludwig@gmail.com>
|
|
* - Fixed module load/unload
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation version 2
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/ioport.h>
|
|
#include <linux/init.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/random.h>
|
|
#include <linux/usb.h>
|
|
#include <linux/videodev2.h>
|
|
#include <media/v4l2-ioctl.h>
|
|
#include <media/tuner.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/kthread.h>
|
|
#include <linux/highmem.h>
|
|
#include <linux/freezer.h>
|
|
|
|
#include "tm6000-regs.h"
|
|
#include "tm6000.h"
|
|
|
|
#define BUFFER_TIMEOUT msecs_to_jiffies(2000) /* 2 seconds */
|
|
|
|
/* Limits minimum and default number of buffers */
|
|
#define TM6000_MIN_BUF 4
|
|
#define TM6000_DEF_BUF 8
|
|
|
|
#define TM6000_NUM_URB_BUF 8
|
|
|
|
#define TM6000_MAX_ISO_PACKETS 46 /* Max number of ISO packets */
|
|
|
|
/* Declare static vars that will be used as parameters */
|
|
static unsigned int vid_limit = 16; /* Video memory limit, in Mb */
|
|
static int video_nr = -1; /* /dev/videoN, -1 for autodetect */
|
|
static int radio_nr = -1; /* /dev/radioN, -1 for autodetect */
|
|
static int keep_urb; /* keep urb buffers allocated */
|
|
|
|
/* Debug level */
|
|
int tm6000_debug;
|
|
EXPORT_SYMBOL_GPL(tm6000_debug);
|
|
|
|
static const struct v4l2_queryctrl no_ctrl = {
|
|
.name = "42",
|
|
.flags = V4L2_CTRL_FLAG_DISABLED,
|
|
};
|
|
|
|
/* supported controls */
|
|
static struct v4l2_queryctrl tm6000_qctrl[] = {
|
|
{
|
|
.id = V4L2_CID_BRIGHTNESS,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Brightness",
|
|
.minimum = 0,
|
|
.maximum = 255,
|
|
.step = 1,
|
|
.default_value = 54,
|
|
.flags = 0,
|
|
}, {
|
|
.id = V4L2_CID_CONTRAST,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Contrast",
|
|
.minimum = 0,
|
|
.maximum = 255,
|
|
.step = 0x1,
|
|
.default_value = 119,
|
|
.flags = 0,
|
|
}, {
|
|
.id = V4L2_CID_SATURATION,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Saturation",
|
|
.minimum = 0,
|
|
.maximum = 255,
|
|
.step = 0x1,
|
|
.default_value = 112,
|
|
.flags = 0,
|
|
}, {
|
|
.id = V4L2_CID_HUE,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
.name = "Hue",
|
|
.minimum = -128,
|
|
.maximum = 127,
|
|
.step = 0x1,
|
|
.default_value = 0,
|
|
.flags = 0,
|
|
},
|
|
/* --- audio --- */
|
|
{
|
|
.id = V4L2_CID_AUDIO_MUTE,
|
|
.name = "Mute",
|
|
.minimum = 0,
|
|
.maximum = 1,
|
|
.type = V4L2_CTRL_TYPE_BOOLEAN,
|
|
}, {
|
|
.id = V4L2_CID_AUDIO_VOLUME,
|
|
.name = "Volume",
|
|
.minimum = -15,
|
|
.maximum = 15,
|
|
.step = 1,
|
|
.default_value = 0,
|
|
.type = V4L2_CTRL_TYPE_INTEGER,
|
|
}
|
|
};
|
|
|
|
static const unsigned int CTRLS = ARRAY_SIZE(tm6000_qctrl);
|
|
static int qctl_regs[ARRAY_SIZE(tm6000_qctrl)];
|
|
|
|
static struct tm6000_fmt format[] = {
|
|
{
|
|
.name = "4:2:2, packed, YVY2",
|
|
.fourcc = V4L2_PIX_FMT_YUYV,
|
|
.depth = 16,
|
|
}, {
|
|
.name = "4:2:2, packed, UYVY",
|
|
.fourcc = V4L2_PIX_FMT_UYVY,
|
|
.depth = 16,
|
|
}, {
|
|
.name = "A/V + VBI mux packet",
|
|
.fourcc = V4L2_PIX_FMT_TM6000,
|
|
.depth = 16,
|
|
}
|
|
};
|
|
|
|
static const struct v4l2_queryctrl *ctrl_by_id(unsigned int id)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < CTRLS; i++)
|
|
if (tm6000_qctrl[i].id == id)
|
|
return tm6000_qctrl+i;
|
|
return NULL;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* DMA and thread functions
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
|
|
#define norm_maxw(a) 720
|
|
#define norm_maxh(a) 576
|
|
|
|
#define norm_minw(a) norm_maxw(a)
|
|
#define norm_minh(a) norm_maxh(a)
|
|
|
|
/*
|
|
* video-buf generic routine to get the next available buffer
|
|
*/
|
|
static inline void get_next_buf(struct tm6000_dmaqueue *dma_q,
|
|
struct tm6000_buffer **buf)
|
|
{
|
|
struct tm6000_core *dev = container_of(dma_q, struct tm6000_core, vidq);
|
|
|
|
if (list_empty(&dma_q->active)) {
|
|
dprintk(dev, V4L2_DEBUG_QUEUE, "No active queue to serve\n");
|
|
*buf = NULL;
|
|
return;
|
|
}
|
|
|
|
*buf = list_entry(dma_q->active.next,
|
|
struct tm6000_buffer, vb.queue);
|
|
}
|
|
|
|
/*
|
|
* Announces that a buffer were filled and request the next
|
|
*/
|
|
static inline void buffer_filled(struct tm6000_core *dev,
|
|
struct tm6000_dmaqueue *dma_q,
|
|
struct tm6000_buffer *buf)
|
|
{
|
|
/* Advice that buffer was filled */
|
|
dprintk(dev, V4L2_DEBUG_ISOC, "[%p/%d] wakeup\n", buf, buf->vb.i);
|
|
buf->vb.state = VIDEOBUF_DONE;
|
|
buf->vb.field_count++;
|
|
do_gettimeofday(&buf->vb.ts);
|
|
|
|
list_del(&buf->vb.queue);
|
|
wake_up(&buf->vb.done);
|
|
}
|
|
|
|
/*
|
|
* Identify the tm5600/6000 buffer header type and properly handles
|
|
*/
|
|
static int copy_streams(u8 *data, unsigned long len,
|
|
struct urb *urb)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = urb->context;
|
|
struct tm6000_core *dev = container_of(dma_q, struct tm6000_core, vidq);
|
|
u8 *ptr = data, *endp = data+len;
|
|
unsigned long header = 0;
|
|
int rc = 0;
|
|
unsigned int cmd, cpysize, pktsize, size, field, block, line, pos = 0;
|
|
struct tm6000_buffer *vbuf = NULL;
|
|
char *voutp = NULL;
|
|
unsigned int linewidth;
|
|
|
|
if (!dev->radio) {
|
|
/* get video buffer */
|
|
get_next_buf(dma_q, &vbuf);
|
|
|
|
if (!vbuf)
|
|
return rc;
|
|
voutp = videobuf_to_vmalloc(&vbuf->vb);
|
|
|
|
if (!voutp)
|
|
return 0;
|
|
}
|
|
|
|
for (ptr = data; ptr < endp;) {
|
|
if (!dev->isoc_ctl.cmd) {
|
|
/* Header */
|
|
if (dev->isoc_ctl.tmp_buf_len > 0) {
|
|
/* from last urb or packet */
|
|
header = dev->isoc_ctl.tmp_buf;
|
|
if (4 - dev->isoc_ctl.tmp_buf_len > 0) {
|
|
memcpy((u8 *)&header +
|
|
dev->isoc_ctl.tmp_buf_len,
|
|
ptr,
|
|
4 - dev->isoc_ctl.tmp_buf_len);
|
|
ptr += 4 - dev->isoc_ctl.tmp_buf_len;
|
|
}
|
|
dev->isoc_ctl.tmp_buf_len = 0;
|
|
} else {
|
|
if (ptr + 3 >= endp) {
|
|
/* have incomplete header */
|
|
dev->isoc_ctl.tmp_buf_len = endp - ptr;
|
|
memcpy(&dev->isoc_ctl.tmp_buf, ptr,
|
|
dev->isoc_ctl.tmp_buf_len);
|
|
return rc;
|
|
}
|
|
/* Seek for sync */
|
|
for (; ptr < endp - 3; ptr++) {
|
|
if (*(ptr + 3) == 0x47)
|
|
break;
|
|
}
|
|
/* Get message header */
|
|
header = *(unsigned long *)ptr;
|
|
ptr += 4;
|
|
}
|
|
|
|
/* split the header fields */
|
|
size = ((header & 0x7e) << 1);
|
|
if (size > 0)
|
|
size -= 4;
|
|
block = (header >> 7) & 0xf;
|
|
field = (header >> 11) & 0x1;
|
|
line = (header >> 12) & 0x1ff;
|
|
cmd = (header >> 21) & 0x7;
|
|
/* Validates haeder fields */
|
|
if (size > TM6000_URB_MSG_LEN)
|
|
size = TM6000_URB_MSG_LEN;
|
|
pktsize = TM6000_URB_MSG_LEN;
|
|
/*
|
|
* calculate position in buffer and change the buffer
|
|
*/
|
|
switch (cmd) {
|
|
case TM6000_URB_MSG_VIDEO:
|
|
if (!dev->radio) {
|
|
if ((dev->isoc_ctl.vfield != field) &&
|
|
(field == 1)) {
|
|
/*
|
|
* Announces that a new buffer
|
|
* were filled
|
|
*/
|
|
buffer_filled(dev, dma_q, vbuf);
|
|
dprintk(dev, V4L2_DEBUG_ISOC,
|
|
"new buffer filled\n");
|
|
get_next_buf(dma_q, &vbuf);
|
|
if (!vbuf)
|
|
return rc;
|
|
voutp = videobuf_to_vmalloc(&vbuf->vb);
|
|
if (!voutp)
|
|
return rc;
|
|
memset(voutp, 0, vbuf->vb.size);
|
|
}
|
|
linewidth = vbuf->vb.width << 1;
|
|
pos = ((line << 1) - field - 1) *
|
|
linewidth + block * TM6000_URB_MSG_LEN;
|
|
/* Don't allow to write out of the buffer */
|
|
if (pos + size > vbuf->vb.size)
|
|
cmd = TM6000_URB_MSG_ERR;
|
|
dev->isoc_ctl.vfield = field;
|
|
}
|
|
break;
|
|
case TM6000_URB_MSG_VBI:
|
|
break;
|
|
case TM6000_URB_MSG_AUDIO:
|
|
case TM6000_URB_MSG_PTS:
|
|
size = pktsize; /* Size is always 180 bytes */
|
|
break;
|
|
}
|
|
} else {
|
|
/* Continue the last copy */
|
|
cmd = dev->isoc_ctl.cmd;
|
|
size = dev->isoc_ctl.size;
|
|
pos = dev->isoc_ctl.pos;
|
|
pktsize = dev->isoc_ctl.pktsize;
|
|
field = dev->isoc_ctl.field;
|
|
}
|
|
cpysize = (endp - ptr > size) ? size : endp - ptr;
|
|
if (cpysize) {
|
|
/* copy data in different buffers */
|
|
switch (cmd) {
|
|
case TM6000_URB_MSG_VIDEO:
|
|
/* Fills video buffer */
|
|
if (vbuf)
|
|
memcpy(&voutp[pos], ptr, cpysize);
|
|
break;
|
|
case TM6000_URB_MSG_AUDIO: {
|
|
int i;
|
|
for (i = 0; i < cpysize; i += 2)
|
|
swab16s((u16 *)(ptr + i));
|
|
|
|
tm6000_call_fillbuf(dev, TM6000_AUDIO, ptr, cpysize);
|
|
break;
|
|
}
|
|
case TM6000_URB_MSG_VBI:
|
|
/* Need some code to copy vbi buffer */
|
|
break;
|
|
case TM6000_URB_MSG_PTS: {
|
|
/* Need some code to copy pts */
|
|
u32 pts;
|
|
pts = *(u32 *)ptr;
|
|
dprintk(dev, V4L2_DEBUG_ISOC, "field %d, PTS %x",
|
|
field, pts);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (ptr + pktsize > endp) {
|
|
/*
|
|
* End of URB packet, but cmd processing is not
|
|
* complete. Preserve the state for a next packet
|
|
*/
|
|
dev->isoc_ctl.pos = pos + cpysize;
|
|
dev->isoc_ctl.size = size - cpysize;
|
|
dev->isoc_ctl.cmd = cmd;
|
|
dev->isoc_ctl.field = field;
|
|
dev->isoc_ctl.pktsize = pktsize - (endp - ptr);
|
|
ptr += endp - ptr;
|
|
} else {
|
|
dev->isoc_ctl.cmd = 0;
|
|
ptr += pktsize;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Identify the tm5600/6000 buffer header type and properly handles
|
|
*/
|
|
static int copy_multiplexed(u8 *ptr, unsigned long len,
|
|
struct urb *urb)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = urb->context;
|
|
struct tm6000_core *dev = container_of(dma_q, struct tm6000_core, vidq);
|
|
unsigned int pos = dev->isoc_ctl.pos, cpysize;
|
|
int rc = 1;
|
|
struct tm6000_buffer *buf;
|
|
char *outp = NULL;
|
|
|
|
get_next_buf(dma_q, &buf);
|
|
if (buf)
|
|
outp = videobuf_to_vmalloc(&buf->vb);
|
|
|
|
if (!outp)
|
|
return 0;
|
|
|
|
while (len > 0) {
|
|
cpysize = min(len, buf->vb.size-pos);
|
|
memcpy(&outp[pos], ptr, cpysize);
|
|
pos += cpysize;
|
|
ptr += cpysize;
|
|
len -= cpysize;
|
|
if (pos >= buf->vb.size) {
|
|
pos = 0;
|
|
/* Announces that a new buffer were filled */
|
|
buffer_filled(dev, dma_q, buf);
|
|
dprintk(dev, V4L2_DEBUG_ISOC, "new buffer filled\n");
|
|
get_next_buf(dma_q, &buf);
|
|
if (!buf)
|
|
break;
|
|
outp = videobuf_to_vmalloc(&(buf->vb));
|
|
if (!outp)
|
|
return rc;
|
|
pos = 0;
|
|
}
|
|
}
|
|
|
|
dev->isoc_ctl.pos = pos;
|
|
return rc;
|
|
}
|
|
|
|
static inline void print_err_status(struct tm6000_core *dev,
|
|
int packet, int status)
|
|
{
|
|
char *errmsg = "Unknown";
|
|
|
|
switch (status) {
|
|
case -ENOENT:
|
|
errmsg = "unlinked synchronuously";
|
|
break;
|
|
case -ECONNRESET:
|
|
errmsg = "unlinked asynchronuously";
|
|
break;
|
|
case -ENOSR:
|
|
errmsg = "Buffer error (overrun)";
|
|
break;
|
|
case -EPIPE:
|
|
errmsg = "Stalled (device not responding)";
|
|
break;
|
|
case -EOVERFLOW:
|
|
errmsg = "Babble (bad cable?)";
|
|
break;
|
|
case -EPROTO:
|
|
errmsg = "Bit-stuff error (bad cable?)";
|
|
break;
|
|
case -EILSEQ:
|
|
errmsg = "CRC/Timeout (could be anything)";
|
|
break;
|
|
case -ETIME:
|
|
errmsg = "Device does not respond";
|
|
break;
|
|
}
|
|
if (packet < 0) {
|
|
dprintk(dev, V4L2_DEBUG_QUEUE, "URB status %d [%s].\n",
|
|
status, errmsg);
|
|
} else {
|
|
dprintk(dev, V4L2_DEBUG_QUEUE, "URB packet %d, status %d [%s].\n",
|
|
packet, status, errmsg);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Controls the isoc copy of each urb packet
|
|
*/
|
|
static inline int tm6000_isoc_copy(struct urb *urb)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = urb->context;
|
|
struct tm6000_core *dev = container_of(dma_q, struct tm6000_core, vidq);
|
|
int i, len = 0, rc = 1, status;
|
|
char *p;
|
|
|
|
if (urb->status < 0) {
|
|
print_err_status(dev, -1, urb->status);
|
|
return 0;
|
|
}
|
|
|
|
for (i = 0; i < urb->number_of_packets; i++) {
|
|
status = urb->iso_frame_desc[i].status;
|
|
|
|
if (status < 0) {
|
|
print_err_status(dev, i, status);
|
|
continue;
|
|
}
|
|
|
|
len = urb->iso_frame_desc[i].actual_length;
|
|
|
|
if (len > 0) {
|
|
p = urb->transfer_buffer + urb->iso_frame_desc[i].offset;
|
|
if (!urb->iso_frame_desc[i].status) {
|
|
if ((dev->fourcc) == V4L2_PIX_FMT_TM6000) {
|
|
rc = copy_multiplexed(p, len, urb);
|
|
if (rc <= 0)
|
|
return rc;
|
|
} else {
|
|
copy_streams(p, len, urb);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* URB control
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
|
|
/*
|
|
* IRQ callback, called by URB callback
|
|
*/
|
|
static void tm6000_irq_callback(struct urb *urb)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = urb->context;
|
|
struct tm6000_core *dev = container_of(dma_q, struct tm6000_core, vidq);
|
|
int i;
|
|
|
|
switch (urb->status) {
|
|
case 0:
|
|
case -ETIMEDOUT:
|
|
break;
|
|
|
|
case -ECONNRESET:
|
|
case -ENOENT:
|
|
case -ESHUTDOWN:
|
|
return;
|
|
|
|
default:
|
|
tm6000_err("urb completion error %d.\n", urb->status);
|
|
break;
|
|
}
|
|
|
|
spin_lock(&dev->slock);
|
|
tm6000_isoc_copy(urb);
|
|
spin_unlock(&dev->slock);
|
|
|
|
/* Reset urb buffers */
|
|
for (i = 0; i < urb->number_of_packets; i++) {
|
|
urb->iso_frame_desc[i].status = 0;
|
|
urb->iso_frame_desc[i].actual_length = 0;
|
|
}
|
|
|
|
urb->status = usb_submit_urb(urb, GFP_ATOMIC);
|
|
if (urb->status)
|
|
tm6000_err("urb resubmit failed (error=%i)\n",
|
|
urb->status);
|
|
}
|
|
|
|
/*
|
|
* Allocate URB buffers
|
|
*/
|
|
static int tm6000_alloc_urb_buffers(struct tm6000_core *dev)
|
|
{
|
|
int num_bufs = TM6000_NUM_URB_BUF;
|
|
int i;
|
|
|
|
if (dev->urb_buffer != NULL)
|
|
return 0;
|
|
|
|
dev->urb_buffer = kmalloc(sizeof(void *)*num_bufs, GFP_KERNEL);
|
|
if (!dev->urb_buffer) {
|
|
tm6000_err("cannot allocate memory for urb buffers\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
dev->urb_dma = kmalloc(sizeof(dma_addr_t *)*num_bufs, GFP_KERNEL);
|
|
if (!dev->urb_dma) {
|
|
tm6000_err("cannot allocate memory for urb dma pointers\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
for (i = 0; i < num_bufs; i++) {
|
|
dev->urb_buffer[i] = usb_alloc_coherent(
|
|
dev->udev, dev->urb_size,
|
|
GFP_KERNEL, &dev->urb_dma[i]);
|
|
if (!dev->urb_buffer[i]) {
|
|
tm6000_err("unable to allocate %i bytes for transfer buffer %i\n",
|
|
dev->urb_size, i);
|
|
return -ENOMEM;
|
|
}
|
|
memset(dev->urb_buffer[i], 0, dev->urb_size);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Free URB buffers
|
|
*/
|
|
static int tm6000_free_urb_buffers(struct tm6000_core *dev)
|
|
{
|
|
int i;
|
|
|
|
if (dev->urb_buffer == NULL)
|
|
return 0;
|
|
|
|
for (i = 0; i < TM6000_NUM_URB_BUF; i++) {
|
|
if (dev->urb_buffer[i]) {
|
|
usb_free_coherent(dev->udev,
|
|
dev->urb_size,
|
|
dev->urb_buffer[i],
|
|
dev->urb_dma[i]);
|
|
dev->urb_buffer[i] = NULL;
|
|
}
|
|
}
|
|
kfree(dev->urb_buffer);
|
|
kfree(dev->urb_dma);
|
|
dev->urb_buffer = NULL;
|
|
dev->urb_dma = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Stop and Deallocate URBs
|
|
*/
|
|
static void tm6000_uninit_isoc(struct tm6000_core *dev)
|
|
{
|
|
struct urb *urb;
|
|
int i;
|
|
|
|
dev->isoc_ctl.buf = NULL;
|
|
for (i = 0; i < dev->isoc_ctl.num_bufs; i++) {
|
|
urb = dev->isoc_ctl.urb[i];
|
|
if (urb) {
|
|
usb_kill_urb(urb);
|
|
usb_unlink_urb(urb);
|
|
usb_free_urb(urb);
|
|
dev->isoc_ctl.urb[i] = NULL;
|
|
}
|
|
dev->isoc_ctl.transfer_buffer[i] = NULL;
|
|
}
|
|
|
|
if (!keep_urb)
|
|
tm6000_free_urb_buffers(dev);
|
|
|
|
kfree(dev->isoc_ctl.urb);
|
|
kfree(dev->isoc_ctl.transfer_buffer);
|
|
|
|
dev->isoc_ctl.urb = NULL;
|
|
dev->isoc_ctl.transfer_buffer = NULL;
|
|
dev->isoc_ctl.num_bufs = 0;
|
|
}
|
|
|
|
/*
|
|
* Assign URBs and start IRQ
|
|
*/
|
|
static int tm6000_prepare_isoc(struct tm6000_core *dev)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = &dev->vidq;
|
|
int i, j, sb_size, pipe, size, max_packets;
|
|
int num_bufs = TM6000_NUM_URB_BUF;
|
|
struct urb *urb;
|
|
|
|
/* De-allocates all pending stuff */
|
|
tm6000_uninit_isoc(dev);
|
|
/* Stop interrupt USB pipe */
|
|
tm6000_ir_int_stop(dev);
|
|
|
|
usb_set_interface(dev->udev,
|
|
dev->isoc_in.bInterfaceNumber,
|
|
dev->isoc_in.bAlternateSetting);
|
|
|
|
/* Start interrupt USB pipe */
|
|
tm6000_ir_int_start(dev);
|
|
|
|
pipe = usb_rcvisocpipe(dev->udev,
|
|
dev->isoc_in.endp->desc.bEndpointAddress &
|
|
USB_ENDPOINT_NUMBER_MASK);
|
|
|
|
size = usb_maxpacket(dev->udev, pipe, usb_pipeout(pipe));
|
|
|
|
if (size > dev->isoc_in.maxsize)
|
|
size = dev->isoc_in.maxsize;
|
|
|
|
dev->isoc_ctl.max_pkt_size = size;
|
|
|
|
max_packets = TM6000_MAX_ISO_PACKETS;
|
|
sb_size = max_packets * size;
|
|
dev->urb_size = sb_size;
|
|
|
|
dev->isoc_ctl.num_bufs = num_bufs;
|
|
|
|
dev->isoc_ctl.urb = kmalloc(sizeof(void *)*num_bufs, GFP_KERNEL);
|
|
if (!dev->isoc_ctl.urb) {
|
|
tm6000_err("cannot alloc memory for usb buffers\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
dev->isoc_ctl.transfer_buffer = kmalloc(sizeof(void *)*num_bufs,
|
|
GFP_KERNEL);
|
|
if (!dev->isoc_ctl.transfer_buffer) {
|
|
tm6000_err("cannot allocate memory for usbtransfer\n");
|
|
kfree(dev->isoc_ctl.urb);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
dprintk(dev, V4L2_DEBUG_QUEUE, "Allocating %d x %d packets"
|
|
" (%d bytes) of %d bytes each to handle %u size\n",
|
|
max_packets, num_bufs, sb_size,
|
|
dev->isoc_in.maxsize, size);
|
|
|
|
|
|
if (!dev->urb_buffer && tm6000_alloc_urb_buffers(dev) < 0) {
|
|
tm6000_err("cannot allocate memory for urb buffers\n");
|
|
|
|
/* call free, as some buffers might have been allocated */
|
|
tm6000_free_urb_buffers(dev);
|
|
kfree(dev->isoc_ctl.urb);
|
|
kfree(dev->isoc_ctl.transfer_buffer);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* allocate urbs and transfer buffers */
|
|
for (i = 0; i < dev->isoc_ctl.num_bufs; i++) {
|
|
urb = usb_alloc_urb(max_packets, GFP_KERNEL);
|
|
if (!urb) {
|
|
tm6000_err("cannot alloc isoc_ctl.urb %i\n", i);
|
|
tm6000_uninit_isoc(dev);
|
|
usb_free_urb(urb);
|
|
return -ENOMEM;
|
|
}
|
|
dev->isoc_ctl.urb[i] = urb;
|
|
|
|
urb->transfer_dma = dev->urb_dma[i];
|
|
dev->isoc_ctl.transfer_buffer[i] = dev->urb_buffer[i];
|
|
|
|
usb_fill_bulk_urb(urb, dev->udev, pipe,
|
|
dev->isoc_ctl.transfer_buffer[i], sb_size,
|
|
tm6000_irq_callback, dma_q);
|
|
urb->interval = dev->isoc_in.endp->desc.bInterval;
|
|
urb->number_of_packets = max_packets;
|
|
urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;
|
|
|
|
for (j = 0; j < max_packets; j++) {
|
|
urb->iso_frame_desc[j].offset = size * j;
|
|
urb->iso_frame_desc[j].length = size;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tm6000_start_thread(struct tm6000_core *dev)
|
|
{
|
|
struct tm6000_dmaqueue *dma_q = &dev->vidq;
|
|
int i;
|
|
|
|
dma_q->frame = 0;
|
|
dma_q->ini_jiffies = jiffies;
|
|
|
|
init_waitqueue_head(&dma_q->wq);
|
|
|
|
/* submit urbs and enables IRQ */
|
|
for (i = 0; i < dev->isoc_ctl.num_bufs; i++) {
|
|
int rc = usb_submit_urb(dev->isoc_ctl.urb[i], GFP_ATOMIC);
|
|
if (rc) {
|
|
tm6000_err("submit of urb %i failed (error=%i)\n", i,
|
|
rc);
|
|
tm6000_uninit_isoc(dev);
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* Videobuf operations
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
|
|
static int
|
|
buffer_setup(struct videobuf_queue *vq, unsigned int *count, unsigned int *size)
|
|
{
|
|
struct tm6000_fh *fh = vq->priv_data;
|
|
|
|
*size = fh->fmt->depth * fh->width * fh->height >> 3;
|
|
if (0 == *count)
|
|
*count = TM6000_DEF_BUF;
|
|
|
|
if (*count < TM6000_MIN_BUF)
|
|
*count = TM6000_MIN_BUF;
|
|
|
|
while (*size * *count > vid_limit * 1024 * 1024)
|
|
(*count)--;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void free_buffer(struct videobuf_queue *vq, struct tm6000_buffer *buf)
|
|
{
|
|
struct tm6000_fh *fh = vq->priv_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
unsigned long flags;
|
|
|
|
if (in_interrupt())
|
|
BUG();
|
|
|
|
/* We used to wait for the buffer to finish here, but this didn't work
|
|
because, as we were keeping the state as VIDEOBUF_QUEUED,
|
|
videobuf_queue_cancel marked it as finished for us.
|
|
(Also, it could wedge forever if the hardware was misconfigured.)
|
|
|
|
This should be safe; by the time we get here, the buffer isn't
|
|
queued anymore. If we ever start marking the buffers as
|
|
VIDEOBUF_ACTIVE, it won't be, though.
|
|
*/
|
|
spin_lock_irqsave(&dev->slock, flags);
|
|
if (dev->isoc_ctl.buf == buf)
|
|
dev->isoc_ctl.buf = NULL;
|
|
spin_unlock_irqrestore(&dev->slock, flags);
|
|
|
|
videobuf_vmalloc_free(&buf->vb);
|
|
buf->vb.state = VIDEOBUF_NEEDS_INIT;
|
|
}
|
|
|
|
static int
|
|
buffer_prepare(struct videobuf_queue *vq, struct videobuf_buffer *vb,
|
|
enum v4l2_field field)
|
|
{
|
|
struct tm6000_fh *fh = vq->priv_data;
|
|
struct tm6000_buffer *buf = container_of(vb, struct tm6000_buffer, vb);
|
|
struct tm6000_core *dev = fh->dev;
|
|
int rc = 0;
|
|
|
|
BUG_ON(NULL == fh->fmt);
|
|
|
|
|
|
/* FIXME: It assumes depth=2 */
|
|
/* The only currently supported format is 16 bits/pixel */
|
|
buf->vb.size = fh->fmt->depth*fh->width*fh->height >> 3;
|
|
if (0 != buf->vb.baddr && buf->vb.bsize < buf->vb.size)
|
|
return -EINVAL;
|
|
|
|
if (buf->fmt != fh->fmt ||
|
|
buf->vb.width != fh->width ||
|
|
buf->vb.height != fh->height ||
|
|
buf->vb.field != field) {
|
|
buf->fmt = fh->fmt;
|
|
buf->vb.width = fh->width;
|
|
buf->vb.height = fh->height;
|
|
buf->vb.field = field;
|
|
buf->vb.state = VIDEOBUF_NEEDS_INIT;
|
|
}
|
|
|
|
if (VIDEOBUF_NEEDS_INIT == buf->vb.state) {
|
|
rc = videobuf_iolock(vq, &buf->vb, NULL);
|
|
if (rc != 0)
|
|
goto fail;
|
|
}
|
|
|
|
if (!dev->isoc_ctl.num_bufs) {
|
|
rc = tm6000_prepare_isoc(dev);
|
|
if (rc < 0)
|
|
goto fail;
|
|
|
|
rc = tm6000_start_thread(dev);
|
|
if (rc < 0)
|
|
goto fail;
|
|
|
|
}
|
|
|
|
buf->vb.state = VIDEOBUF_PREPARED;
|
|
return 0;
|
|
|
|
fail:
|
|
free_buffer(vq, buf);
|
|
return rc;
|
|
}
|
|
|
|
static void
|
|
buffer_queue(struct videobuf_queue *vq, struct videobuf_buffer *vb)
|
|
{
|
|
struct tm6000_buffer *buf = container_of(vb, struct tm6000_buffer, vb);
|
|
struct tm6000_fh *fh = vq->priv_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
struct tm6000_dmaqueue *vidq = &dev->vidq;
|
|
|
|
buf->vb.state = VIDEOBUF_QUEUED;
|
|
list_add_tail(&buf->vb.queue, &vidq->active);
|
|
}
|
|
|
|
static void buffer_release(struct videobuf_queue *vq, struct videobuf_buffer *vb)
|
|
{
|
|
struct tm6000_buffer *buf = container_of(vb, struct tm6000_buffer, vb);
|
|
|
|
free_buffer(vq, buf);
|
|
}
|
|
|
|
static struct videobuf_queue_ops tm6000_video_qops = {
|
|
.buf_setup = buffer_setup,
|
|
.buf_prepare = buffer_prepare,
|
|
.buf_queue = buffer_queue,
|
|
.buf_release = buffer_release,
|
|
};
|
|
|
|
/* ------------------------------------------------------------------
|
|
* IOCTL handling
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
|
|
static bool is_res_read(struct tm6000_core *dev, struct tm6000_fh *fh)
|
|
{
|
|
/* Is the current fh handling it? if so, that's OK */
|
|
if (dev->resources == fh && dev->is_res_read)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool is_res_streaming(struct tm6000_core *dev, struct tm6000_fh *fh)
|
|
{
|
|
/* Is the current fh handling it? if so, that's OK */
|
|
if (dev->resources == fh)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool res_get(struct tm6000_core *dev, struct tm6000_fh *fh,
|
|
bool is_res_read)
|
|
{
|
|
/* Is the current fh handling it? if so, that's OK */
|
|
if (dev->resources == fh && dev->is_res_read == is_res_read)
|
|
return true;
|
|
|
|
/* is it free? */
|
|
if (dev->resources)
|
|
return false;
|
|
|
|
/* grab it */
|
|
dev->resources = fh;
|
|
dev->is_res_read = is_res_read;
|
|
dprintk(dev, V4L2_DEBUG_RES_LOCK, "res: get\n");
|
|
return true;
|
|
}
|
|
|
|
static void res_free(struct tm6000_core *dev, struct tm6000_fh *fh)
|
|
{
|
|
/* Is the current fh handling it? if so, that's OK */
|
|
if (dev->resources != fh)
|
|
return;
|
|
|
|
dev->resources = NULL;
|
|
dprintk(dev, V4L2_DEBUG_RES_LOCK, "res: put\n");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
* IOCTL vidioc handling
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
static int vidioc_querycap(struct file *file, void *priv,
|
|
struct v4l2_capability *cap)
|
|
{
|
|
struct tm6000_core *dev = ((struct tm6000_fh *)priv)->dev;
|
|
|
|
strlcpy(cap->driver, "tm6000", sizeof(cap->driver));
|
|
strlcpy(cap->card, "Trident TVMaster TM5600/6000/6010", sizeof(cap->card));
|
|
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE |
|
|
V4L2_CAP_STREAMING |
|
|
V4L2_CAP_AUDIO |
|
|
V4L2_CAP_READWRITE;
|
|
|
|
if (dev->tuner_type != TUNER_ABSENT)
|
|
cap->capabilities |= V4L2_CAP_TUNER;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_enum_fmt_vid_cap(struct file *file, void *priv,
|
|
struct v4l2_fmtdesc *f)
|
|
{
|
|
if (unlikely(f->index >= ARRAY_SIZE(format)))
|
|
return -EINVAL;
|
|
|
|
strlcpy(f->description, format[f->index].name, sizeof(f->description));
|
|
f->pixelformat = format[f->index].fourcc;
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_g_fmt_vid_cap(struct file *file, void *priv,
|
|
struct v4l2_format *f)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
|
|
f->fmt.pix.width = fh->width;
|
|
f->fmt.pix.height = fh->height;
|
|
f->fmt.pix.field = fh->vb_vidq.field;
|
|
f->fmt.pix.pixelformat = fh->fmt->fourcc;
|
|
f->fmt.pix.bytesperline =
|
|
(f->fmt.pix.width * fh->fmt->depth) >> 3;
|
|
f->fmt.pix.sizeimage =
|
|
f->fmt.pix.height * f->fmt.pix.bytesperline;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct tm6000_fmt *format_by_fourcc(unsigned int fourcc)
|
|
{
|
|
unsigned int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(format); i++)
|
|
if (format[i].fourcc == fourcc)
|
|
return format+i;
|
|
return NULL;
|
|
}
|
|
|
|
static int vidioc_try_fmt_vid_cap(struct file *file, void *priv,
|
|
struct v4l2_format *f)
|
|
{
|
|
struct tm6000_core *dev = ((struct tm6000_fh *)priv)->dev;
|
|
struct tm6000_fmt *fmt;
|
|
enum v4l2_field field;
|
|
|
|
fmt = format_by_fourcc(f->fmt.pix.pixelformat);
|
|
if (NULL == fmt) {
|
|
dprintk(dev, V4L2_DEBUG_IOCTL_ARG, "Fourcc format (0x%08x)"
|
|
" invalid.\n", f->fmt.pix.pixelformat);
|
|
return -EINVAL;
|
|
}
|
|
|
|
field = f->fmt.pix.field;
|
|
|
|
if (field == V4L2_FIELD_ANY)
|
|
field = V4L2_FIELD_SEQ_TB;
|
|
else if (V4L2_FIELD_INTERLACED != field) {
|
|
dprintk(dev, V4L2_DEBUG_IOCTL_ARG, "Field type invalid.\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
tm6000_get_std_res(dev);
|
|
|
|
f->fmt.pix.width = dev->width;
|
|
f->fmt.pix.height = dev->height;
|
|
|
|
f->fmt.pix.width &= ~0x01;
|
|
|
|
f->fmt.pix.field = field;
|
|
|
|
f->fmt.pix.bytesperline =
|
|
(f->fmt.pix.width * fmt->depth) >> 3;
|
|
f->fmt.pix.sizeimage =
|
|
f->fmt.pix.height * f->fmt.pix.bytesperline;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*FIXME: This seems to be generic enough to be at videodev2 */
|
|
static int vidioc_s_fmt_vid_cap(struct file *file, void *priv,
|
|
struct v4l2_format *f)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
int ret = vidioc_try_fmt_vid_cap(file, fh, f);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
fh->fmt = format_by_fourcc(f->fmt.pix.pixelformat);
|
|
fh->width = f->fmt.pix.width;
|
|
fh->height = f->fmt.pix.height;
|
|
fh->vb_vidq.field = f->fmt.pix.field;
|
|
fh->type = f->type;
|
|
|
|
dev->fourcc = f->fmt.pix.pixelformat;
|
|
|
|
tm6000_set_fourcc_format(dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_reqbufs(struct file *file, void *priv,
|
|
struct v4l2_requestbuffers *p)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
|
|
return videobuf_reqbufs(&fh->vb_vidq, p);
|
|
}
|
|
|
|
static int vidioc_querybuf(struct file *file, void *priv,
|
|
struct v4l2_buffer *p)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
|
|
return videobuf_querybuf(&fh->vb_vidq, p);
|
|
}
|
|
|
|
static int vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *p)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
|
|
return videobuf_qbuf(&fh->vb_vidq, p);
|
|
}
|
|
|
|
static int vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *p)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
|
|
return videobuf_dqbuf(&fh->vb_vidq, p,
|
|
file->f_flags & O_NONBLOCK);
|
|
}
|
|
|
|
static int vidioc_streamon(struct file *file, void *priv, enum v4l2_buf_type i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (fh->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
|
|
return -EINVAL;
|
|
if (i != fh->type)
|
|
return -EINVAL;
|
|
|
|
if (!res_get(dev, fh, false))
|
|
return -EBUSY;
|
|
return videobuf_streamon(&fh->vb_vidq);
|
|
}
|
|
|
|
static int vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (fh->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
|
|
return -EINVAL;
|
|
|
|
if (i != fh->type)
|
|
return -EINVAL;
|
|
|
|
videobuf_streamoff(&fh->vb_vidq);
|
|
res_free(dev, fh);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_s_std(struct file *file, void *priv, v4l2_std_id *norm)
|
|
{
|
|
int rc = 0;
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
dev->norm = *norm;
|
|
rc = tm6000_init_analog_mode(dev);
|
|
|
|
fh->width = dev->width;
|
|
fh->height = dev->height;
|
|
|
|
if (rc < 0)
|
|
return rc;
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, core, s_std, dev->norm);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const char *iname[] = {
|
|
[TM6000_INPUT_TV] = "Television",
|
|
[TM6000_INPUT_COMPOSITE1] = "Composite 1",
|
|
[TM6000_INPUT_COMPOSITE2] = "Composite 2",
|
|
[TM6000_INPUT_SVIDEO] = "S-Video",
|
|
};
|
|
|
|
static int vidioc_enum_input(struct file *file, void *priv,
|
|
struct v4l2_input *i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
unsigned int n;
|
|
|
|
n = i->index;
|
|
if (n >= 3)
|
|
return -EINVAL;
|
|
|
|
if (!dev->vinput[n].type)
|
|
return -EINVAL;
|
|
|
|
i->index = n;
|
|
|
|
if (dev->vinput[n].type == TM6000_INPUT_TV)
|
|
i->type = V4L2_INPUT_TYPE_TUNER;
|
|
else
|
|
i->type = V4L2_INPUT_TYPE_CAMERA;
|
|
|
|
strcpy(i->name, iname[dev->vinput[n].type]);
|
|
|
|
i->std = TM6000_STD;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_g_input(struct file *file, void *priv, unsigned int *i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
*i = dev->input;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_s_input(struct file *file, void *priv, unsigned int i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
int rc = 0;
|
|
|
|
if (i >= 3)
|
|
return -EINVAL;
|
|
if (!dev->vinput[i].type)
|
|
return -EINVAL;
|
|
|
|
dev->input = i;
|
|
|
|
rc = vidioc_s_std(file, priv, &dev->vfd->current_norm);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* --- controls ---------------------------------------------- */
|
|
static int vidioc_queryctrl(struct file *file, void *priv,
|
|
struct v4l2_queryctrl *qc)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(tm6000_qctrl); i++)
|
|
if (qc->id && qc->id == tm6000_qctrl[i].id) {
|
|
memcpy(qc, &(tm6000_qctrl[i]),
|
|
sizeof(*qc));
|
|
return 0;
|
|
}
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int vidioc_g_ctrl(struct file *file, void *priv,
|
|
struct v4l2_control *ctrl)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
int val;
|
|
|
|
/* FIXME: Probably, those won't work! Maybe we need shadow regs */
|
|
switch (ctrl->id) {
|
|
case V4L2_CID_CONTRAST:
|
|
val = tm6000_get_reg(dev, TM6010_REQ07_R08_LUMA_CONTRAST_ADJ, 0);
|
|
break;
|
|
case V4L2_CID_BRIGHTNESS:
|
|
val = tm6000_get_reg(dev, TM6010_REQ07_R09_LUMA_BRIGHTNESS_ADJ, 0);
|
|
return 0;
|
|
case V4L2_CID_SATURATION:
|
|
val = tm6000_get_reg(dev, TM6010_REQ07_R0A_CHROMA_SATURATION_ADJ, 0);
|
|
return 0;
|
|
case V4L2_CID_HUE:
|
|
val = tm6000_get_reg(dev, TM6010_REQ07_R0B_CHROMA_HUE_PHASE_ADJ, 0);
|
|
return 0;
|
|
case V4L2_CID_AUDIO_MUTE:
|
|
val = dev->ctl_mute;
|
|
return 0;
|
|
case V4L2_CID_AUDIO_VOLUME:
|
|
val = dev->ctl_volume;
|
|
return 0;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (val < 0)
|
|
return val;
|
|
|
|
ctrl->value = val;
|
|
|
|
return 0;
|
|
}
|
|
static int vidioc_s_ctrl(struct file *file, void *priv,
|
|
struct v4l2_control *ctrl)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
u8 val = ctrl->value;
|
|
|
|
switch (ctrl->id) {
|
|
case V4L2_CID_CONTRAST:
|
|
tm6000_set_reg(dev, TM6010_REQ07_R08_LUMA_CONTRAST_ADJ, val);
|
|
return 0;
|
|
case V4L2_CID_BRIGHTNESS:
|
|
tm6000_set_reg(dev, TM6010_REQ07_R09_LUMA_BRIGHTNESS_ADJ, val);
|
|
return 0;
|
|
case V4L2_CID_SATURATION:
|
|
tm6000_set_reg(dev, TM6010_REQ07_R0A_CHROMA_SATURATION_ADJ, val);
|
|
return 0;
|
|
case V4L2_CID_HUE:
|
|
tm6000_set_reg(dev, TM6010_REQ07_R0B_CHROMA_HUE_PHASE_ADJ, val);
|
|
return 0;
|
|
case V4L2_CID_AUDIO_MUTE:
|
|
dev->ctl_mute = val;
|
|
tm6000_tvaudio_set_mute(dev, val);
|
|
return 0;
|
|
case V4L2_CID_AUDIO_VOLUME:
|
|
dev->ctl_volume = val;
|
|
tm6000_set_volume(dev, val);
|
|
return 0;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static int vidioc_g_tuner(struct file *file, void *priv,
|
|
struct v4l2_tuner *t)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (unlikely(UNSET == dev->tuner_type))
|
|
return -EINVAL;
|
|
if (0 != t->index)
|
|
return -EINVAL;
|
|
|
|
strcpy(t->name, "Television");
|
|
t->type = V4L2_TUNER_ANALOG_TV;
|
|
t->capability = V4L2_TUNER_CAP_NORM;
|
|
t->rangehigh = 0xffffffffUL;
|
|
t->rxsubchans = V4L2_TUNER_SUB_STEREO;
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, g_tuner, t);
|
|
|
|
t->audmode = dev->amode;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_s_tuner(struct file *file, void *priv,
|
|
struct v4l2_tuner *t)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (UNSET == dev->tuner_type)
|
|
return -EINVAL;
|
|
if (0 != t->index)
|
|
return -EINVAL;
|
|
|
|
dev->amode = t->audmode;
|
|
dprintk(dev, 3, "audio mode: %x\n", t->audmode);
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, s_tuner, t);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_g_frequency(struct file *file, void *priv,
|
|
struct v4l2_frequency *f)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (unlikely(UNSET == dev->tuner_type))
|
|
return -EINVAL;
|
|
|
|
f->type = fh->radio ? V4L2_TUNER_RADIO : V4L2_TUNER_ANALOG_TV;
|
|
f->frequency = dev->freq;
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, g_frequency, f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidioc_s_frequency(struct file *file, void *priv,
|
|
struct v4l2_frequency *f)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (unlikely(UNSET == dev->tuner_type))
|
|
return -EINVAL;
|
|
if (unlikely(f->tuner != 0))
|
|
return -EINVAL;
|
|
if (0 == fh->radio && V4L2_TUNER_ANALOG_TV != f->type)
|
|
return -EINVAL;
|
|
if (1 == fh->radio && V4L2_TUNER_RADIO != f->type)
|
|
return -EINVAL;
|
|
|
|
dev->freq = f->frequency;
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, s_frequency, f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_querycap(struct file *file, void *priv,
|
|
struct v4l2_capability *cap)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
strcpy(cap->driver, "tm6000");
|
|
strlcpy(cap->card, dev->name, sizeof(dev->name));
|
|
sprintf(cap->bus_info, "USB%04x:%04x",
|
|
le16_to_cpu(dev->udev->descriptor.idVendor),
|
|
le16_to_cpu(dev->udev->descriptor.idProduct));
|
|
cap->version = dev->dev_type;
|
|
cap->capabilities = V4L2_CAP_TUNER |
|
|
V4L2_CAP_AUDIO |
|
|
V4L2_CAP_RADIO |
|
|
V4L2_CAP_READWRITE |
|
|
V4L2_CAP_STREAMING;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_g_tuner(struct file *file, void *priv,
|
|
struct v4l2_tuner *t)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (0 != t->index)
|
|
return -EINVAL;
|
|
|
|
memset(t, 0, sizeof(*t));
|
|
strcpy(t->name, "Radio");
|
|
t->type = V4L2_TUNER_RADIO;
|
|
t->rxsubchans = V4L2_TUNER_SUB_STEREO;
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, g_tuner, t);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_s_tuner(struct file *file, void *priv,
|
|
struct v4l2_tuner *t)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (0 != t->index)
|
|
return -EINVAL;
|
|
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, s_tuner, t);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_enum_input(struct file *file, void *priv,
|
|
struct v4l2_input *i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (i->index != 0)
|
|
return -EINVAL;
|
|
|
|
if (!dev->rinput.type)
|
|
return -EINVAL;
|
|
|
|
strcpy(i->name, "Radio");
|
|
i->type = V4L2_INPUT_TYPE_TUNER;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_g_input(struct file *filp, void *priv, unsigned int *i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (dev->input != 5)
|
|
return -EINVAL;
|
|
|
|
*i = dev->input - 5;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_g_audio(struct file *file, void *priv,
|
|
struct v4l2_audio *a)
|
|
{
|
|
memset(a, 0, sizeof(*a));
|
|
strcpy(a->name, "Radio");
|
|
return 0;
|
|
}
|
|
|
|
static int radio_s_audio(struct file *file, void *priv,
|
|
const struct v4l2_audio *a)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int radio_s_input(struct file *filp, void *priv, unsigned int i)
|
|
{
|
|
struct tm6000_fh *fh = priv;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (i)
|
|
return -EINVAL;
|
|
|
|
if (!dev->rinput.type)
|
|
return -EINVAL;
|
|
|
|
dev->input = i + 5;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int radio_s_std(struct file *file, void *fh, v4l2_std_id *norm)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int radio_queryctrl(struct file *file, void *priv,
|
|
struct v4l2_queryctrl *c)
|
|
{
|
|
const struct v4l2_queryctrl *ctrl;
|
|
|
|
if (c->id < V4L2_CID_BASE ||
|
|
c->id >= V4L2_CID_LASTP1)
|
|
return -EINVAL;
|
|
if (c->id == V4L2_CID_AUDIO_MUTE) {
|
|
ctrl = ctrl_by_id(c->id);
|
|
*c = *ctrl;
|
|
} else
|
|
*c = no_ctrl;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------
|
|
File operations for the device
|
|
------------------------------------------------------------------*/
|
|
|
|
static int __tm6000_open(struct file *file)
|
|
{
|
|
struct video_device *vdev = video_devdata(file);
|
|
struct tm6000_core *dev = video_drvdata(file);
|
|
struct tm6000_fh *fh;
|
|
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
int i, rc;
|
|
int radio = 0;
|
|
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "tm6000: open called (dev=%s)\n",
|
|
video_device_node_name(vdev));
|
|
|
|
switch (vdev->vfl_type) {
|
|
case VFL_TYPE_GRABBER:
|
|
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
break;
|
|
case VFL_TYPE_VBI:
|
|
type = V4L2_BUF_TYPE_VBI_CAPTURE;
|
|
break;
|
|
case VFL_TYPE_RADIO:
|
|
radio = 1;
|
|
break;
|
|
}
|
|
|
|
/* If more than one user, mutex should be added */
|
|
dev->users++;
|
|
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "open dev=%s type=%s users=%d\n",
|
|
video_device_node_name(vdev), v4l2_type_names[type],
|
|
dev->users);
|
|
|
|
/* allocate + initialize per filehandle data */
|
|
fh = kzalloc(sizeof(*fh), GFP_KERNEL);
|
|
if (NULL == fh) {
|
|
dev->users--;
|
|
return -ENOMEM;
|
|
}
|
|
|
|
file->private_data = fh;
|
|
fh->dev = dev;
|
|
fh->radio = radio;
|
|
dev->radio = radio;
|
|
fh->type = type;
|
|
dev->fourcc = format[0].fourcc;
|
|
|
|
fh->fmt = format_by_fourcc(dev->fourcc);
|
|
|
|
tm6000_get_std_res(dev);
|
|
|
|
fh->width = dev->width;
|
|
fh->height = dev->height;
|
|
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "Open: fh=0x%08lx, dev=0x%08lx, "
|
|
"dev->vidq=0x%08lx\n",
|
|
(unsigned long)fh, (unsigned long)dev,
|
|
(unsigned long)&dev->vidq);
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "Open: list_empty "
|
|
"queued=%d\n", list_empty(&dev->vidq.queued));
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "Open: list_empty "
|
|
"active=%d\n", list_empty(&dev->vidq.active));
|
|
|
|
/* initialize hardware on analog mode */
|
|
rc = tm6000_init_analog_mode(dev);
|
|
if (rc < 0)
|
|
return rc;
|
|
|
|
if (dev->mode != TM6000_MODE_ANALOG) {
|
|
/* Put all controls at a sane state */
|
|
for (i = 0; i < ARRAY_SIZE(tm6000_qctrl); i++)
|
|
qctl_regs[i] = tm6000_qctrl[i].default_value;
|
|
|
|
dev->mode = TM6000_MODE_ANALOG;
|
|
}
|
|
|
|
if (!fh->radio) {
|
|
videobuf_queue_vmalloc_init(&fh->vb_vidq, &tm6000_video_qops,
|
|
NULL, &dev->slock,
|
|
fh->type,
|
|
V4L2_FIELD_INTERLACED,
|
|
sizeof(struct tm6000_buffer), fh, &dev->lock);
|
|
} else {
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "video_open: setting radio device\n");
|
|
dev->input = 5;
|
|
tm6000_set_audio_rinput(dev);
|
|
v4l2_device_call_all(&dev->v4l2_dev, 0, tuner, s_radio);
|
|
tm6000_prepare_isoc(dev);
|
|
tm6000_start_thread(dev);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tm6000_open(struct file *file)
|
|
{
|
|
struct video_device *vdev = video_devdata(file);
|
|
int res;
|
|
|
|
mutex_lock(vdev->lock);
|
|
res = __tm6000_open(file);
|
|
mutex_unlock(vdev->lock);
|
|
return res;
|
|
}
|
|
|
|
static ssize_t
|
|
tm6000_read(struct file *file, char __user *data, size_t count, loff_t *pos)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
|
|
if (fh->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {
|
|
int res;
|
|
|
|
if (!res_get(fh->dev, fh, true))
|
|
return -EBUSY;
|
|
|
|
if (mutex_lock_interruptible(&dev->lock))
|
|
return -ERESTARTSYS;
|
|
res = videobuf_read_stream(&fh->vb_vidq, data, count, pos, 0,
|
|
file->f_flags & O_NONBLOCK);
|
|
mutex_unlock(&dev->lock);
|
|
return res;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int
|
|
__tm6000_poll(struct file *file, struct poll_table_struct *wait)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_buffer *buf;
|
|
|
|
if (V4L2_BUF_TYPE_VIDEO_CAPTURE != fh->type)
|
|
return POLLERR;
|
|
|
|
if (!!is_res_streaming(fh->dev, fh))
|
|
return POLLERR;
|
|
|
|
if (!is_res_read(fh->dev, fh)) {
|
|
/* streaming capture */
|
|
if (list_empty(&fh->vb_vidq.stream))
|
|
return POLLERR;
|
|
buf = list_entry(fh->vb_vidq.stream.next, struct tm6000_buffer, vb.stream);
|
|
} else {
|
|
/* read() capture */
|
|
return videobuf_poll_stream(file, &fh->vb_vidq, wait);
|
|
}
|
|
poll_wait(file, &buf->vb.done, wait);
|
|
if (buf->vb.state == VIDEOBUF_DONE ||
|
|
buf->vb.state == VIDEOBUF_ERROR)
|
|
return POLLIN | POLLRDNORM;
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int tm6000_poll(struct file *file, struct poll_table_struct *wait)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
unsigned int res;
|
|
|
|
mutex_lock(&dev->lock);
|
|
res = __tm6000_poll(file, wait);
|
|
mutex_unlock(&dev->lock);
|
|
return res;
|
|
}
|
|
|
|
static int tm6000_release(struct file *file)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
struct video_device *vdev = video_devdata(file);
|
|
|
|
dprintk(dev, V4L2_DEBUG_OPEN, "tm6000: close called (dev=%s, users=%d)\n",
|
|
video_device_node_name(vdev), dev->users);
|
|
|
|
mutex_lock(&dev->lock);
|
|
dev->users--;
|
|
|
|
res_free(dev, fh);
|
|
|
|
if (!dev->users) {
|
|
tm6000_uninit_isoc(dev);
|
|
|
|
/* Stop interrupt USB pipe */
|
|
tm6000_ir_int_stop(dev);
|
|
|
|
usb_reset_configuration(dev->udev);
|
|
|
|
if (dev->int_in.endp)
|
|
usb_set_interface(dev->udev,
|
|
dev->isoc_in.bInterfaceNumber, 2);
|
|
else
|
|
usb_set_interface(dev->udev,
|
|
dev->isoc_in.bInterfaceNumber, 0);
|
|
|
|
/* Start interrupt USB pipe */
|
|
tm6000_ir_int_start(dev);
|
|
|
|
if (!fh->radio)
|
|
videobuf_mmap_free(&fh->vb_vidq);
|
|
}
|
|
|
|
kfree(fh);
|
|
mutex_unlock(&dev->lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tm6000_mmap(struct file *file, struct vm_area_struct * vma)
|
|
{
|
|
struct tm6000_fh *fh = file->private_data;
|
|
struct tm6000_core *dev = fh->dev;
|
|
int res;
|
|
|
|
if (mutex_lock_interruptible(&dev->lock))
|
|
return -ERESTARTSYS;
|
|
res = videobuf_mmap_mapper(&fh->vb_vidq, vma);
|
|
mutex_unlock(&dev->lock);
|
|
return res;
|
|
}
|
|
|
|
static struct v4l2_file_operations tm6000_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = tm6000_open,
|
|
.release = tm6000_release,
|
|
.unlocked_ioctl = video_ioctl2, /* V4L2 ioctl handler */
|
|
.read = tm6000_read,
|
|
.poll = tm6000_poll,
|
|
.mmap = tm6000_mmap,
|
|
};
|
|
|
|
static const struct v4l2_ioctl_ops video_ioctl_ops = {
|
|
.vidioc_querycap = vidioc_querycap,
|
|
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
|
|
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
|
|
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
|
|
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
|
|
.vidioc_s_std = vidioc_s_std,
|
|
.vidioc_enum_input = vidioc_enum_input,
|
|
.vidioc_g_input = vidioc_g_input,
|
|
.vidioc_s_input = vidioc_s_input,
|
|
.vidioc_queryctrl = vidioc_queryctrl,
|
|
.vidioc_g_ctrl = vidioc_g_ctrl,
|
|
.vidioc_s_ctrl = vidioc_s_ctrl,
|
|
.vidioc_g_tuner = vidioc_g_tuner,
|
|
.vidioc_s_tuner = vidioc_s_tuner,
|
|
.vidioc_g_frequency = vidioc_g_frequency,
|
|
.vidioc_s_frequency = vidioc_s_frequency,
|
|
.vidioc_streamon = vidioc_streamon,
|
|
.vidioc_streamoff = vidioc_streamoff,
|
|
.vidioc_reqbufs = vidioc_reqbufs,
|
|
.vidioc_querybuf = vidioc_querybuf,
|
|
.vidioc_qbuf = vidioc_qbuf,
|
|
.vidioc_dqbuf = vidioc_dqbuf,
|
|
};
|
|
|
|
static struct video_device tm6000_template = {
|
|
.name = "tm6000",
|
|
.fops = &tm6000_fops,
|
|
.ioctl_ops = &video_ioctl_ops,
|
|
.release = video_device_release,
|
|
.tvnorms = TM6000_STD,
|
|
.current_norm = V4L2_STD_NTSC_M,
|
|
};
|
|
|
|
static const struct v4l2_file_operations radio_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = tm6000_open,
|
|
.release = tm6000_release,
|
|
.unlocked_ioctl = video_ioctl2,
|
|
};
|
|
|
|
static const struct v4l2_ioctl_ops radio_ioctl_ops = {
|
|
.vidioc_querycap = radio_querycap,
|
|
.vidioc_g_tuner = radio_g_tuner,
|
|
.vidioc_enum_input = radio_enum_input,
|
|
.vidioc_g_audio = radio_g_audio,
|
|
.vidioc_s_tuner = radio_s_tuner,
|
|
.vidioc_s_audio = radio_s_audio,
|
|
.vidioc_s_input = radio_s_input,
|
|
.vidioc_s_std = radio_s_std,
|
|
.vidioc_queryctrl = radio_queryctrl,
|
|
.vidioc_g_input = radio_g_input,
|
|
.vidioc_g_ctrl = vidioc_g_ctrl,
|
|
.vidioc_s_ctrl = vidioc_s_ctrl,
|
|
.vidioc_g_frequency = vidioc_g_frequency,
|
|
.vidioc_s_frequency = vidioc_s_frequency,
|
|
};
|
|
|
|
static struct video_device tm6000_radio_template = {
|
|
.name = "tm6000",
|
|
.fops = &radio_fops,
|
|
.ioctl_ops = &radio_ioctl_ops,
|
|
};
|
|
|
|
/* -----------------------------------------------------------------
|
|
* Initialization and module stuff
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
|
|
static struct video_device *vdev_init(struct tm6000_core *dev,
|
|
const struct video_device
|
|
*template, const char *type_name)
|
|
{
|
|
struct video_device *vfd;
|
|
|
|
vfd = video_device_alloc();
|
|
if (NULL == vfd)
|
|
return NULL;
|
|
|
|
*vfd = *template;
|
|
vfd->v4l2_dev = &dev->v4l2_dev;
|
|
vfd->release = video_device_release;
|
|
vfd->debug = tm6000_debug;
|
|
vfd->lock = &dev->lock;
|
|
|
|
snprintf(vfd->name, sizeof(vfd->name), "%s %s", dev->name, type_name);
|
|
|
|
video_set_drvdata(vfd, dev);
|
|
return vfd;
|
|
}
|
|
|
|
int tm6000_v4l2_register(struct tm6000_core *dev)
|
|
{
|
|
int ret = -1;
|
|
|
|
dev->vfd = vdev_init(dev, &tm6000_template, "video");
|
|
|
|
if (!dev->vfd) {
|
|
printk(KERN_INFO "%s: can't register video device\n",
|
|
dev->name);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* init video dma queues */
|
|
INIT_LIST_HEAD(&dev->vidq.active);
|
|
INIT_LIST_HEAD(&dev->vidq.queued);
|
|
|
|
ret = video_register_device(dev->vfd, VFL_TYPE_GRABBER, video_nr);
|
|
|
|
if (ret < 0) {
|
|
printk(KERN_INFO "%s: can't register video device\n",
|
|
dev->name);
|
|
return ret;
|
|
}
|
|
|
|
printk(KERN_INFO "%s: registered device %s\n",
|
|
dev->name, video_device_node_name(dev->vfd));
|
|
|
|
if (dev->caps.has_radio) {
|
|
dev->radio_dev = vdev_init(dev, &tm6000_radio_template,
|
|
"radio");
|
|
if (!dev->radio_dev) {
|
|
printk(KERN_INFO "%s: can't register radio device\n",
|
|
dev->name);
|
|
ret = -ENXIO;
|
|
return ret; /* FIXME release resource */
|
|
}
|
|
|
|
ret = video_register_device(dev->radio_dev, VFL_TYPE_RADIO,
|
|
radio_nr);
|
|
if (ret < 0) {
|
|
printk(KERN_INFO "%s: can't register radio device\n",
|
|
dev->name);
|
|
return ret; /* FIXME release resource */
|
|
}
|
|
|
|
printk(KERN_INFO "%s: registered device %s\n",
|
|
dev->name, video_device_node_name(dev->radio_dev));
|
|
}
|
|
|
|
printk(KERN_INFO "Trident TVMaster TM5600/TM6000/TM6010 USB2 board (Load status: %d)\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
int tm6000_v4l2_unregister(struct tm6000_core *dev)
|
|
{
|
|
video_unregister_device(dev->vfd);
|
|
|
|
/* if URB buffers are still allocated free them now */
|
|
tm6000_free_urb_buffers(dev);
|
|
|
|
if (dev->radio_dev) {
|
|
if (video_is_registered(dev->radio_dev))
|
|
video_unregister_device(dev->radio_dev);
|
|
else
|
|
video_device_release(dev->radio_dev);
|
|
dev->radio_dev = NULL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int tm6000_v4l2_exit(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
module_param(video_nr, int, 0);
|
|
MODULE_PARM_DESC(video_nr, "Allow changing video device number");
|
|
|
|
module_param_named(debug, tm6000_debug, int, 0444);
|
|
MODULE_PARM_DESC(debug, "activates debug info");
|
|
|
|
module_param(vid_limit, int, 0644);
|
|
MODULE_PARM_DESC(vid_limit, "capture memory limit in megabytes");
|
|
|
|
module_param(keep_urb, bool, 0);
|
|
MODULE_PARM_DESC(keep_urb, "Keep urb buffers allocated even when the device is closed by the user");
|