从0写USB摄像头驱动程序
1.构造一个usb_driver结构体
.id_table
.probe
1.分配video_device结构体
2.设置
3.注册
2.下面具体分析probe函数中的内容:
定义:static struct video_device *myuvc_vdev;
myuvc_vdev=video_device_alloc();
注册:video_register_device(myuvc_vdev,VFL_TYPE_GRABBER,-1);
设置操作待会再说。
3.在myuvc_disconnect函数中:
Video_unregister_device(myuvc_vdev);
video_device_release(myuvc_vdev);
4.probe函数中的设置操作有:
myuvc_vdev->release=myuvc_release;
myuvc_vdev->fops=&myuvc_fops;
myuvc_vdev->ioctl_fops=&myuvc_ioctl_fops
涉及的定义有:
static const struct v4l2_file_operations myuvc_fops = {
.owner = THIS_MODULE,
.open = myuvc_open,
.release = myuvc_close,
.mmap = myuvc_mmap,
.ioctl = video_ioctl2, /* V4L2 ioctl handler */
.poll = myuvc_poll,
};
static const struct v4l2_ioctl_ops myuvc_ioctl_ops = {
// 表示它是一个摄像头设备
.vidioc_querycap = myuvc_vidioc_querycap,
/* 用于列举、获得、测试、设置摄像头的数据的格式 */
.vidioc_enum_fmt_vid_cap = myuvc_vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = myuvc_vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = myuvc_vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = myuvc_vidioc_s_fmt_vid_cap,
/* 缓冲区操作: 申请/查询/放入队列/取出队列 */
.vidioc_reqbufs = myuvc_vidioc_reqbufs,
.vidioc_querybuf = myuvc_vidioc_querybuf,
.vidioc_qbuf = myuvc_vidioc_qbuf,
.vidioc_dqbuf = myuvc_vidioc_dqbuf,
// 启动/停止
.vidioc_streamon = myuvc_vidioc_streamon,
.vidioc_streamoff = myuvc_vidioc_streamoff,
};
当应用层调用open等函数时会执行驱动中myuvc_fops结构体中的相应函数;当应用层调用一些ioctl函数时会执行驱动的myuvc_fops结构体中的成员video_ioctl2,它又会去调用myuvc_ioctl_ops结构体中的相应ioctl函数。
5.注意:ioctl缓冲区操作步骤中:先分配缓冲区,然后再进行mmap地址映射,映射到用户空间中,然后再将缓冲区放入队列。
6.关于poll函数:APP通过poll/select确定有数据后,把缓存从队列中取出来。之前已经通过mmap映射了缓存,APP可以直接读数据。读完数据后再把换酬勤放入队列。然后进行上述循环。
如何打开虚拟机的USB服务:
先点击虚拟机界面中菜单栏中的VM选项。
VM->Removable Devices ->USB2.0Camera -> Connect
如果卸载ubuntu上原来的UVC驱动程序:
Rmmod uvcvideo
所谓的UVC规范就是接到电脑上不用装驱动的摄像头。
下面来细讲每个ioctl函数:
/****************************/
static int myuvc_vidioc_querycap(struct file *file, void *priv,
struct v4l2_capability *cap)
{
memset(cap, 0, sizeof *cap);
strcpy(cap->driver, "myuvc");
strcpy(cap->card, "myuvc");
cap->version = 1;
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
return 0;
}
最重要的是最后一句说明摄像头是捕获设备以及通过ioctl和mmap映射等方式读取设备而不是通过read,write等函数来获取数据。
/******************************/
/* A3 列举支持哪种格式
* 参考: uvc_fmts 数组
*/
static int myuvc_vidioc_enum_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
/* 人工查看描述符可知我们用的摄像头只支持1种格式 */
if (f->index >= 1)
return -EINVAL;
/* 支持什么格式呢?
* 查看VideoStreaming Interface的描述符,
* 得到GUID为"59 55 59 32 00 00 10 00 80 00 00 aa 00 38 9b 71"
*/
strcpy(f->description, "4:2:2, packed, YUYV");
f->pixelformat = V4L2_PIX_FMT_YUYV;
return 0;
}
在uvc_fmts数组中可找到GUID与格式的对应关系。可知当前摄像头是YUYV格式。
/****************************/
static struct v4l2_format myuvc_format;//全局变量
/* A4 返回当前所使用的格式 */
static int myuvc_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
memcpy(f, &myuvc_format, sizeof(myuvc_format));
return (0);
}
设置的全局变量用于保存当前的格式,要获取当前的格式时,只要将这个全局变量拷贝出去即可。
/******************************************************************************/
static struct frame_desc frames[] = {{640, 480}, {352, 288}, {320, 240}, {176, 144}, {160, 120}};
static int frame_idx = 1;
static int bBitsPerPixel = 16; /* lsusb -v -d 0x1e4e: "bBitsPerPixel" */
/* A5 测试驱动程序是否支持某种格式, 强制设置该格式
* 参考: uvc_v4l2_try_format
* myvivi_vidioc_try_fmt_vid_cap
*/
static int myuvc_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
{
return -EINVAL;
}
if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV)
return -EINVAL;
/* 调整format的width, height,
* 计算bytesperline, sizeimage
*/
/* 人工查看描述符, 确定支持哪几种分辨率 */
f->fmt.pix.width = frames[frame_idx].width;
f->fmt.pix.height = frames[frame_idx].height;
f->fmt.pix.bytesperline =
(f->fmt.pix.width * bBitsPerPixel) >> 3;
f->fmt.pix.sizeimage =
f->fmt.pix.height * f->fmt.pix.bytesperline;
return 0;
}
try中强制设定的格式内容包括是不是捕捉设备、是不是YUYV格式、设置图像的分辨率一个像素点的位宽。
/******************************/
/* A6 参考 myvivi_vidioc_s_fmt_vid_cap */
static int myuvc_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
int ret = myuvc_vidioc_try_fmt_vid_cap(file, NULL, f);
if (ret < 0)
return ret;
memcpy(&myuvc_format, f, sizeof(myuvc_format));
return 0;
}
设置格式就是把期望的格式复制到全局变量中,复制前先try一下是否符合要求。然后再进行复制。故正确的逻辑顺序是应用程序设置格式,然后再才能获得格式,不然全局变量是空的。
/*****************************/
/* 参考uvc_video_queue定义一些结构体 */
struct myuvc_buffer {
struct v4l2_buffer buf;
int state;
int vma_use_count; /* 表示是否已经被mmap */
wait_queue_head_t wait; /* APP要读某个缓冲区,如果无数据,在此休眠 */
struct list_head stream;
struct list_head irq;
};
struct myuvc_queue {
void *mem;
int count;
int buf_size;
struct myuvc_buffer buffer[32];
struct urb *urb[32];
char *urb_buffer[32];
dma_addr_t urb_dma[32];
unsigned int urb_size;
struct list_head mainqueue; /* 供APP消费用 */
struct list_head irqqueue; /* 供底层驱动生产用 */
};
/* A7 APP调用该ioctl让驱动程序分配若干个缓存, APP将从这些缓存中读到视频数据
* 参考: uvc_alloc_buffers
*/
static int myuvc_vidioc_reqbufs(struct file *file, void *priv,
struct v4l2_requestbuffers *p)
{
int nbuffers = p->count;//缓冲区个数
int bufsize = PAGE_ALIGN(myuvc_format.fmt.pix.sizeimage);//一个缓冲区大小
unsigned int i;
void *mem = NULL;//所有缓冲区的起始地址
int ret;
if ((ret = myuvc_free_buffers()) < 0)
goto done;
/* Bail out if no buffers should be allocated. */
if (nbuffers == 0)
goto done;
/* Decrement the number of buffers until allocation succeeds. */
for (; nbuffers > 0; --nbuffers) {//分配缓冲区,逐级降低分配
mem = vmalloc_32(nbuffers * bufsize);
if (mem != NULL)
break;
}
if (mem == NULL) {
ret = -ENOMEM;
goto done;
}
/* 这些缓存是一次性作为一个整体来分配的 */
memset(&myuvc_queue, 0, sizeof(myuvc_queue));
INIT_LIST_HEAD(&myuvc_queue.mainqueue);
INIT_LIST_HEAD(&myuvc_queue.irqqueue);
for (i = 0; i < nbuffers; ++i) { //设置每个缓冲区
myuvc_queue.buffer[i].buf.index = i;
myuvc_queue.buffer[i].buf.m.offset = i * bufsize;
myuvc_queue.buffer[i].buf.length = myuvc_format.fmt.pix.sizeimage;
myuvc_queue.buffer[i].buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
myuvc_queue.buffer[i].buf.sequence = 0;
myuvc_queue.buffer[i].buf.field = V4L2_FIELD_NONE;
myuvc_queue.buffer[i].buf.memory = V4L2_MEMORY_MMAP;
myuvc_queue.buffer[i].buf.flags = 0;
myuvc_queue.buffer[i].state = VIDEOBUF_IDLE;
init_waitqueue_head(&myuvc_queue.buffer[i].wait);
}
myuvc_queue.mem = mem;
myuvc_queue.count = nbuffers;
myuvc_queue.buf_size = bufsize;
ret = nbuffers;
done:
return ret;
}
分配缓冲区/****************************/
static int myuvc_free_buffers(void)
{
if (myuvc_queue.mem)
{
vfree(myuvc_queue.mem);
memset(&myuvc_queue, 0, sizeof(myuvc_queue));
myuvc_queue.mem = NULL;
}
return 0;
}
在上面的分配缓冲区时先被调用,如果已经分配过缓冲区,则要先释放之前分配的缓冲区。
/****************************/
/* A8 查询缓存状态, 比如地址信息(APP可以用mmap进行映射)
* 参考 uvc_query_buffer
*/
static int myuvc_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
int ret = 0;
if (v4l2_buf->index >= myuvc_queue.count) {
ret = -EINVAL;
goto done;
}
memcpy(v4l2_buf, &myuvc_queue.buffer[v4l2_buf->index].buf, sizeof(*v4l2_buf));
/* 更新flags */
if (myuvc_queue.buffer[v4l2_buf->index].vma_use_count)
v4l2_buf->flags |= V4L2_BUF_FLAG_MAPPED;
switch (myuvc_queue.buffer[v4l2_buf->index].state) {
case VIDEOBUF_ERROR:
case VIDEOBUF_DONE:
v4l2_buf->flags |= V4L2_BUF_FLAG_DONE;
break;
case VIDEOBUF_QUEUED:
case VIDEOBUF_ACTIVE:
v4l2_buf->flags |= V4L2_BUF_FLAG_QUEUED;
break;
case VIDEOBUF_IDLE:
default:
break;
}
done:
return ret;
}
这个查询函数根据缓冲区的序列号找到相应的缓冲区并全部拷贝出来。除此之外还要获得相应的标志位如是否done,是否放入队列,地址是否映射等等,方便以后使用。
/****************************/
/* A10 把缓冲区放入队列, 底层的硬件操作函数将会把数据放入这个队列的缓存
* 参考: uvc_queue_buffer
*/
static int myuvc_vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{
struct myuvc_buffer *buf;
int ret;
/* 0. APP传入的v4l2_buf可能有问题, 要做判断 */
if (v4l2_buf->type != V4L2_BUF_TYPE_VIDEO_CAPTURE ||
v4l2_buf->memory != V4L2_MEMORY_MMAP) {
return -EINVAL;
}
if (v4l2_buf->index >= myuvc_queue.count) {
return -EINVAL;
}
buf = &myuvc_queue.buffer[v4l2_buf->index];
if (buf->state != VIDEOBUF_IDLE) {
return -EINVAL;
}
/* 1. 修改状态 */
buf->state = VIDEOBUF_QUEUED;
buf->buf.bytesused = 0;
/* 2. 放入2个队列 */
/* 队列1: 供APP使用
* 当缓冲区没有数据时,放入mainqueue队列
* 当缓冲区有数据时, APP从mainqueue队列中取出
*/
list_add_tail(&buf->stream, &myuvc_queue.mainqueue);
/* 队列2: 供产生数据的函数使用
* 当采集到数据时,从irqqueue队列中取出第1个缓冲区,存入数据
*/
list_add_tail(&buf->irq, &myuvc_queue.irqqueue);
return 0;
}
/********************************************/
启动摄像头:
1.向USB摄像头设置参数,如使用哪个format,那个frame。这些参数虽然在vidioc_s_fmt这个ioctl函数中被赋值给了一个结构体,但是并未发送到USB硬件中,而这里的streamon启动函数中才真正将这个结构体的信息发送到USB硬件中。