近期将之前学习嵌入式的笔记进行了整理,内容涉及到基础知识以及嵌入式开发过程中比较重要的模块开发知识,文章中有我在学习过程中的标注,非常详细,可以让入门学习事半功倍。

获取链接 提取码:qk7t

一、从零写USB摄像头驱动—分析描述符

USB设备插入电脑后,电脑上就会相应的显示其是某种设备(U盘,摄像头,信号采集卡等等),表明这些设备“身份”的就叫做设备描述符(就是一些格式化的数据)。操作系统通过底层的USB总线驱动程序就访问/读取这些描述符信息。


上图是USB设备通用的描述符j结构,对于USB摄像头,其中会有一些自己定义的描述符(主要指摄像头中用于控制和传输数据的接口描述符)。

注意:这些USB设备的描述符在USB设备接入的时候由USB总线驱动程序全部读取出来存放到了内存中,因此可以直接引用它们。

USB摄像头的描述符:

现在首先开始搭建UVC驱动程序的框架结构:

UVC驱动程序以USB设备驱动程序为基础:接上usb设备后,USB设备驱动程序会在usb_driver结构体–>id_table中查找是否能够支持该设备,如果可以,就调用其中的probe函数进行下一步操作:

id_table:表示该usb设备驱动程序所能支持的USB设备。

在uvc_driver.c文件中定义了id_table,首先是针对一些特定厂家的,最后有关于UVC通用类的设备的id_table:


这里的通用表示:如果接入的usb设备接口信息满足 USB_INTERFACE_INFO(USB_CLASS_VIDEO, 1, 0) 的条件,就可以支持。详细解读:
bInterfaceClass接口类为:USB_CLASS_VIDEO
bInterfaceSubClass接口子类为:1
bInterfaceProtocol接口协议为:0

查阅UVC规范:

发现这里是“1”,表示子类为视频控制接口 VideoControl Interface ,没有规定视频数据流接口(VideoStreaming Interface)的原因是VC Interface和VS Interface 是配对的,且每一个VS Interface 都从属于一个VC Interface,因此找到了VC Interface就可以找到对应的VS Interface。

也可以加入VS Interface:

写好后编译装载驱动程序:

发现probe函数被调用了两次,是因为USB摄像头中有两个接口(VC和VS)。

使用 lsusb 工具可以将USB设备的详细信息打印出来,因此分析该工具的源码可以帮助我们打印描述符信息:

lsusb.c:
maindumpdevdump_devicedump_configfor (i = 0 ; i < config->bNumInterfaces ; i++)  //一个配置下可能有多个接口Interfaces,将多个接口的描述符都打印出来dump_interface(dev, &config->interface[i]);for (i = 0; i < interface->num_altsetting; i++)  //一个接口中又可能有多个设置altsetting,将多个设置描述符打印出来dump_altsetting(dev, &interface->altsetting[i]);

可以结合图2-2和UVC 1.5 Class specification文档来一个一个分析USB摄像头的描述符信息。

1. 打印出了设备描述符和配置描述符:

这些描述符被usb总线驱动程序读取出来以后都保存在内存里,可以随时取出打印出来。
只有一个设备和一个配置描述符:

2. IAD:接口联合体描述符:

一个usb摄像头有可能有不同的接口(VideoControl 接口、VideoStreaming接口),IAD描述符中就讲解这些接口。

3. 接口描述符usb_host_interface desc :


probe函数的参数就有接口,如果这个接口能够被struct usb_driver myuvc_driver 所支持(和id_table中的项匹配),那么它就会作为一个参数传入probe函数,因此就可以直接使用该参数。

另外,一个接口可能有多种设置,正在使用的设置就在cur_altsetting 中。
在lsusb源码中也有体现:对于每一个配置中的每一个接口,打印接口中的设置参数:

1)VC 控制接口描述符:只有一个设置

2)VS 视频流接口描述符:共有13个设置

灰色框表示的是UVC规范自己定义的描述符

这些自定义的描述符都存在哪里呢?
–>所有那些UVC规范中所定义的描述符都存在一个buffer中:

VideoControl Interface的自定义描述符:

extra buffer of interface 0:
extra desc 0: 0d 24 01 00 01 4d 00 80 c3 c9 01 01 01 VC_HEADER
extra desc 1: 12 24 02                 01 01 02 00 00 00 00 00 00 00 00 03 0e 00 00 VC_INPUT_TERMINAL  ID
extra desc 2: 09 24 03                 02 01 01          00             04         00 VC_OUTPUT_TERMINAL ID wTerminalType  bAssocTerminal bSourceID
extra desc 3: 0b 24 05                 03 01         00 00           02           7f 14      00 VC_PROCESSING_UNIT ID bSourceID  wMaxMultiplier  bControlSize bmControls
extra desc 4: 1a 24 06                 04 ad cc b1 c2 f6 ab b8 48 8e 37 32 d4 f3 a3 fe ec 08            01        03         01 3f 00VC_EXTENSION_UNIT  ID GUID                                            bNumControls  bNrInPins baSourceIDIT(01)  ===>  PU(03)  ===>  EU(04)  ===>  OT(02)

这里还有一个问题,这些功能组件(IT、PU、EU、OT)是怎么联系起来的呢?

以PU为例:PU的描述符中有一个SourceID,表示PU组件的数据来源,查看读取到的描述符信息,发现我们摄像头中PU的数据来源是01,正好是IT的UnitID值。

第一个字节表示描述符的长度;
第二个字节表示:这里的类自定义接口的宏 CS_INTERFACE 在这里都一样:均为0x24;

第三个字节表示自定义描述符的子类型,对于VC接口中不同的实体(entity,CT、PU、IT、OT、ST)都有不同的值对应:


VC_DESCRIPTOR_UNDEFINED 0x00
VC_HEADER 0x01
VC_INPUT_TERMINAL 0x02
VC_OUTPUT_TERMINAL 0x03
VC_SELECTOR_UNIT 0x04
VC_PROCESSING_UNIT 0x05
VC_EXTENSION_UNIT 0x06
VC_ENCODING_UNIT 0x07

VideoStreaming Interface的自定义描述符:

extra buffer of interface 1:
extra desc 0: 0e 24 01              01 df 00 81 00 02 02 01 01 01 00 VS_INPUT_HEADER bNumFormats
extra desc 1: 1b 24 04                     01           05                   59 55 59 32 00 00 10 00 80 00 00 aa 00 38 9b 71  10              01 00 00 00 00 VS_FORMAT_UNCOMPRESSED bFormatIndex bNumFrameDescriptors GUID                                             bBitsPerPixel
extra desc 2: 1e 24 05                     01          00              80 02   e0 01   00 00 ca 08 00 00 ca 08 00 60 09 00 15 16 05 00 01 15 16 05 00 VS_FRAME_UNCOMPRESSED  bFrameIndex bmCapabilities  wWidth  wHeight  640x480
extra desc 3: 1e 24 05 02 00 60 01 20 01 00 80 e6 02 00 80 e6 02 00 18 03 00 15 16 05 00 01 15 16 05 00 VS_FRAME_UNCOMPRESSED
extra desc 4: 1e 24 05 03 00 40 01 f0 00 00 80 32 02 00 80 32 02 00 58 02 00 15 16 05 00 01 15 16 05 00
extra desc 5: 1e 24 05 04 00 b0 00 90 00 00 a0 b9 00 00 a0 b9 00 00 c6 00 00 15 16 05 00 01 15 16 05 00
extra desc 6: 1e 24 05 05 00 a0 00 78 00 00 a0 8c 00 00 a0 8c 00 00 96 00 00 15 16 05 00 01 15 16 05 00 extra desc 7: 1a 24 03 00 05 80 02 e0 01 60 01 20 01 40 01 f0 00 b0 00 90 00 a0 00 78 00 00 VS_STILL_IMAGE_FRAME
extra desc 8: 06 24 0d 01 01 04 

注意:这里需要注意的是,一个摄像头可能支持多种格式FORMAT,每一种格式下又可能出现多种分辨率FRAME。

VS_INPUT_HEADER 0x01
VS_STILL_IMAGE_FRAME 0x03
VS_FORMAT_UNCOMPRESSED 0x04
VS_FRAME_UNCOMPRESSED 0x05
VS_COLORFORMAT 0x0D

4. Interrupt_Endpoint 端点描述符:

总结:从描述符的分析中我们可以知道:
1)该USB摄像头的厂家ID、支持的协议、电源参数等硬件信息
2)支持哪些格式,哪些分辨率等等信息,并且是否支持调整亮度等等功能。

二、从零写USB摄像头驱动—实现数据传输_框架

※ 补充内容:
为了避免每次使用dmesg命令去查看内核的打印信息,用以下方法解决:

A.设置ubuntu让它从串口0输出printk信息
a. 设置vmware添加serial port, 使用文件作为串口
b. 启动ubuntu,修改/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX=“console=tty0 console=ttyS0,115200n8”

sudo update-grub
sudo reboot

c. ubuntu禁止root用户登录
先修改root密码: sudo passwd root
然后执行"su root"就可以用root登录了

d. echo “8 4 1 7” > /proc/sys/kernel/printk

再次重启后,只要执行这2个命令就可以:
su root
echo “8 4 1 7” > /proc/sys/kernel/printk

按照以上设置后就可以在文件中实时看到内核的打印信息了。

1. 总体框架

1.构造一个usb_driver
2.设置probe:2.1. 分配video_device:video_device_alloc2.2. 设置.fops.ioctl_ops (里面需要设置11项)如果要用内核提供的缓冲区操作函数,还需要构造一个videobuf_queue_ops2.3. 注册: video_register_device      id_table: 表示支持哪些USB设备
3.注册: usb_register

具体代码仿照之前写的 myvivi.c 来进行:



三、从零写USB摄像头驱动—实现数据传输_简单函数

编写的顺序与上一节框架中讲解的函数调用顺序一致。


A3 列举支持哪种格式

/* 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;
}
/* 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);
}

A5 测试驱动程序是否支持某种格式, 并强制设置该格式

/* 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;
}

设置参数:

/* 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;
}

A7 APP调用该ioctl让驱动程序分配若干个缓存, APP将从这些缓存中读到视频数据

/* 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;   //其中每一个buffer的偏移地址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;  // 一整块buffer的起始地址myuvc_queue.count = nbuffers;myuvc_queue.buf_size = bufsize;ret = nbuffers;done:return ret;
}

A8 查询缓存状态, 比如地址信息(APP可以用mmap进行映射)

/* 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;
}

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;
}

图解:

定义这两个队列:

添加队列节点:

初始化:

A13 APP通过poll/select确定有数据后, 把缓存从队列中取出来

/* 参考: uvc_dequeue_buffer */

static int myuvc_vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)
{/* APP发现数据就绪后, 从mainqueue里取出这个buffer */struct myuvc_buffer *buf;int ret = 0;if (list_empty(&myuvc_queue.mainqueue)) {ret = -EINVAL;goto done;}buf = list_first_entry(&myuvc_queue.mainqueue, struct myuvc_buffer, stream);switch (buf->state) {case VIDEOBUF_ERROR:ret = -EIO;case VIDEOBUF_DONE:buf->state = VIDEOBUF_IDLE;break;case VIDEOBUF_IDLE:case VIDEOBUF_QUEUED:case VIDEOBUF_ACTIVE:default:ret = -EINVAL;goto done;}list_del(&buf->stream);  //直接将有数据的buf从队列中删除done:return ret;
}

接下来进行启动streamon 操作(最复杂)**

/* A11 启动传输 */

参考: uvc_video_enable(video, 1):uvc_commit_videouvc_init_video
static int myuvc_vidioc_streamon(struct file *file, void *priv, enum v4l2_buf_type i)
{int ret;/* 1. 向USB摄像头设置参数: 比如使用哪个format, 使用这个format下的哪个frame(分辨率) * 参考: uvc_set_video_ctrl / uvc_get_video_ctrl* 1.1 根据一个结构体uvc_streaming_control设置数据包: 可以手工设置,也可以发起一个usb传输,从usb设备上读出后再修改* 1.2 调用usb_control_msg发出数据包*//* a. 测试参数 */ret = myuvc_try_streaming_params(&myuvc_params);printk("myuvc_try_streaming_params ret = %d\n", ret);/* b. 取出参数, 在测试参数和设置参数之间必须加入获取参数的步骤,否则会设置不成功*/ ret = myuvc_get_streaming_params(&myuvc_params);printk("myuvc_get_streaming_params ret = %d\n", ret);/* c. 设置参数 */ret = myuvc_set_streaming_params(&myuvc_params);printk("myuvc_set_streaming_params ret = %d\n", ret);myuvc_print_streaming_params(&myuvc_params);/* d. 设置VideoStreaming Interface所使用的setting* d.1 从myuvc_params确定带宽 * d.2 根据setting的endpoint能传输的wMaxPacketSize*     找到能满足该带宽的setting*//* 手工确定:* bandwidth = myuvc_params.dwMaxPayloadTransferSize = 1024* * 观察lsusb -v -d 0x1e4e:的结果:*                wMaxPacketSize     0x0400  1x 1024 bytes* * bAlternateSetting       8*/usb_set_interface(myuvc_udev, myuvc_streaming_intf, myuvc_streaming_bAlternateSetting);/* 2. 分配设置URB */ret = myuvc_alloc_init_urbs();if (ret)printk("myuvc_alloc_init_urbs err : ret = %d\n", ret);/* 3. 提交URB以接收数据:有很多个urb需要全部提交上去 */  /* 仿照uvc_init_video */for (i = 0; i < MYUVC_URBS; ++i) {if ((ret = usb_submit_urb(myuvc_queue.urb[i], GFP_KERNEL)) < 0) {printk("Failed to submit URB %u (%d).\n", i, ret);myuvc_uninit_urbs();return ret;}}return 0;
}

三、从零写USB摄像头驱动—实现数据传输_设置参数

1)编写取出(获得)参数的函数myuvc_get_streaming_params

/* 参考: uvc_get_video_ctrl (ret = uvc_get_video_ctrl(video, probe, 1, GET_CUR)) static int uvc_get_video_ctrl(struct uvc_video_device *video,struct uvc_streaming_control *ctrl, int probe, __u8 query)*/
static int myuvc_get_streaming_params(struct myuvc_streaming_control *ctrl)
{__u8 *data;__u16 size;int ret;__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;size = uvc_version >= 0x0110 ? 34 : 26;data = kmalloc(size, GFP_KERNEL);if (data == NULL)return -ENOMEM;pipe = (GET_CUR & 0x80) ? usb_rcvctrlpipe(myuvc_udev, 0): usb_sndctrlpipe(myuvc_udev, 0);type |= (GET_CUR & 0x80) ? USB_DIR_IN : USB_DIR_OUT;ret = usb_control_msg(myuvc_udev, pipe, GET_CUR, type, VS_PROBE_CONTROL << 8,0 << 8 | myuvc_streaming_intf, data, size, 5000);if (ret < 0)goto done;ctrl->bmHint = le16_to_cpup((__le16 *)&data[0]);ctrl->bFormatIndex = data[2];ctrl->bFrameIndex = data[3];ctrl->dwFrameInterval = le32_to_cpup((__le32 *)&data[4]);ctrl->wKeyFrameRate = le16_to_cpup((__le16 *)&data[8]);ctrl->wPFrameRate = le16_to_cpup((__le16 *)&data[10]);ctrl->wCompQuality = le16_to_cpup((__le16 *)&data[12]);ctrl->wCompWindowSize = le16_to_cpup((__le16 *)&data[14]);ctrl->wDelay = le16_to_cpup((__le16 *)&data[16]);ctrl->dwMaxVideoFrameSize = get_unaligned_le32(&data[18]);ctrl->dwMaxPayloadTransferSize = get_unaligned_le32(&data[22]);if (size == 34) {ctrl->dwClockFrequency = get_unaligned_le32(&data[26]);ctrl->bmFramingInfo = data[30];ctrl->bPreferedVersion = data[31];ctrl->bMinVersion = data[32];ctrl->bMaxVersion = data[33];} else {//ctrl->dwClockFrequency = video->dev->clock_frequency;ctrl->bmFramingInfo = 0;ctrl->bPreferedVersion = 0;ctrl->bMinVersion = 0;ctrl->bMaxVersion = 0;}done:kfree(data);return (ret < 0) ? ret : 0;
}

2)编写打印参数的函数myuvc_print_streaming_params

static void myuvc_print_streaming_params(struct myuvc_streaming_control *ctrl)
{printk("video params:\n");printk("bmHint                   = %d\n", ctrl->bmHint);printk("bFormatIndex             = %d\n", ctrl->bFormatIndex);printk("bFrameIndex              = %d\n", ctrl->bFrameIndex);printk("dwFrameInterval          = %d\n", ctrl->dwFrameInterval);printk("wKeyFrameRate            = %d\n", ctrl->wKeyFrameRate);printk("wPFrameRate              = %d\n", ctrl->wPFrameRate);printk("wCompQuality             = %d\n", ctrl->wCompQuality);printk("wCompWindowSize          = %d\n", ctrl->wCompWindowSize);printk("wDelay                   = %d\n", ctrl->wDelay);printk("dwMaxVideoFrameSize      = %d\n", ctrl->dwMaxVideoFrameSize);printk("dwMaxPayloadTransferSize = %d\n", ctrl->dwMaxPayloadTransferSize);printk("dwClockFrequency         = %d\n", ctrl->dwClockFrequency);printk("bmFramingInfo            = %d\n", ctrl->bmFramingInfo);printk("bPreferedVersion         = %d\n", ctrl->bPreferedVersion);printk("bMinVersion              = %d\n", ctrl->bMinVersion);printk("bMinVersion              = %d\n", ctrl->bMinVersion);
}

编译装载驱动后,从打印出的数据分析来看,还有很多问题:

3)编写测试参数的函数myuvc_try_streaming_params

协商过程:设置的参数能不能用,要先发给USB摄像头先确认能否使用,如果不能用,则再次修改发送。确认了之后才设置让摄像头工作与新参数下

帧间隙:


/* 参考: uvc_v4l2_try_format ∕uvc_probe_video *       uvc_set_video_ctrl(video, probe, 1)*/
static int myuvc_try_streaming_params(struct myuvc_streaming_control *ctrl)
{__u8 *data;__u16 size;int ret;__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;memset(ctrl, 0, sizeof *ctrl);ctrl->bmHint = 1;  /* dwFrameInterval */ctrl->bFormatIndex = 1;ctrl->bFrameIndex  = frame_idx + 1;ctrl->dwFrameInterval = 333333;size = uvc_version >= 0x0110 ? 34 : 26;data = kzalloc(size, GFP_KERNEL);if (data == NULL)return -ENOMEM;*(__le16 *)&data[0] = cpu_to_le16(ctrl->bmHint);data[2] = ctrl->bFormatIndex;data[3] = ctrl->bFrameIndex;*(__le32 *)&data[4] = cpu_to_le32(ctrl->dwFrameInterval);*(__le16 *)&data[8] = cpu_to_le16(ctrl->wKeyFrameRate);*(__le16 *)&data[10] = cpu_to_le16(ctrl->wPFrameRate);*(__le16 *)&data[12] = cpu_to_le16(ctrl->wCompQuality);*(__le16 *)&data[14] = cpu_to_le16(ctrl->wCompWindowSize);*(__le16 *)&data[16] = cpu_to_le16(ctrl->wDelay);put_unaligned_le32(ctrl->dwMaxVideoFrameSize, &data[18]);put_unaligned_le32(ctrl->dwMaxPayloadTransferSize, &data[22]);if (size == 34) {put_unaligned_le32(ctrl->dwClockFrequency, &data[26]);data[30] = ctrl->bmFramingInfo;data[31] = ctrl->bPreferedVersion;data[32] = ctrl->bMinVersion;data[33] = ctrl->bMaxVersion;}pipe = (SET_CUR & 0x80) ? usb_rcvctrlpipe(myuvc_udev, 0): usb_sndctrlpipe(myuvc_udev, 0);type |= (SET_CUR & 0x80) ? USB_DIR_IN : USB_DIR_OUT;ret = usb_control_msg(myuvc_udev, pipe, SET_CUR, type, VS_PROBE_CONTROL << 8,0 << 8 | myuvc_streaming_intf, data, size, 5000);kfree(data);return (ret < 0) ? ret : 0;}

4)编写设置参数的函数myuvc_set_streaming_params

写法跟测试参数的函数类似:

/* 参考: uvc_v4l2_try_format ∕uvc_probe_video *       uvc_set_video_ctrl(video, probe, 1)*/
static int myuvc_set_streaming_params(struct myuvc_streaming_control *ctrl)
{__u8 *data;__u16 size;int ret;__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;size = uvc_version >= 0x0110 ? 34 : 26;data = kzalloc(size, GFP_KERNEL);if (data == NULL)return -ENOMEM;*(__le16 *)&data[0] = cpu_to_le16(ctrl->bmHint);data[2] = ctrl->bFormatIndex;data[3] = ctrl->bFrameIndex;*(__le32 *)&data[4] = cpu_to_le32(ctrl->dwFrameInterval);*(__le16 *)&data[8] = cpu_to_le16(ctrl->wKeyFrameRate);*(__le16 *)&data[10] = cpu_to_le16(ctrl->wPFrameRate);*(__le16 *)&data[12] = cpu_to_le16(ctrl->wCompQuality);*(__le16 *)&data[14] = cpu_to_le16(ctrl->wCompWindowSize);*(__le16 *)&data[16] = cpu_to_le16(ctrl->wDelay);put_unaligned_le32(ctrl->dwMaxVideoFrameSize, &data[18]);put_unaligned_le32(ctrl->dwMaxPayloadTransferSize, &data[22]);if (size == 34) {put_unaligned_le32(ctrl->dwClockFrequency, &data[26]);data[30] = ctrl->bmFramingInfo;data[31] = ctrl->bPreferedVersion;data[32] = ctrl->bMinVersion;data[33] = ctrl->bMaxVersion;}pipe = (SET_CUR & 0x80) ? usb_rcvctrlpipe(myuvc_udev, 0): usb_sndctrlpipe(myuvc_udev, 0);type |= (SET_CUR & 0x80) ? USB_DIR_IN : USB_DIR_OUT;//之前是PROBE,现在是COMMITret = usb_control_msg(myuvc_udev, pipe, SET_CUR, type, VS_COMMIT_CONTROL << 8,0 << 8 | myuvc_streaming_intf, data, size, 5000);kfree(data);return (ret < 0) ? ret : 0;}

编译装载后可以看到:

四、从零写USB摄像头驱动—实现数据传输_urb

1)首先写出urb初始化的函数:myuvc_alloc_init_urbs:分配设置urb

/* 参考: uvc_init_video_isoc */
static int myuvc_alloc_init_urbs(void)
{u16 psize;u32 size;int npackets;int i;int j;struct urb *urb;psize = wMaxPacketSize; /* 实时传输端点一次能传输的最大字节数 */、size  = myuvc_params.dwMaxVideoFrameSize;  /* 一帧数据的最大长度 */npackets = DIV_ROUND_UP(size, psize);if (npackets > 32)npackets = 32;size = myuvc_queue.urb_size = psize * npackets;for (i = 0; i < MYUVC_URBS; ++i) {/* 1. 分配usb_buffers,为每一个urb分配usb_buffer */ //分配成功后,buffer的物理地址会存在myuvc_queue.urb_dma[i]中myuvc_queue.urb_buffer[i] = usb_buffer_alloc(myuvc_udev, size,GFP_KERNEL | __GFP_NOWARN, &myuvc_queue.urb_dma[i]);/* 2. 分配urb */myuvc_queue.urb[i] = usb_alloc_urb(npackets, GFP_KERNEL);if (!myuvc_queue.urb_buffer[i] || !myuvc_queue.urb[i]){myuvc_uninit_urbs();return -ENOMEM;}}/* 3. 设置urb */ for (i = 0; i < MYUVC_URBS; ++i) {urb = myuvc_queue.urb[i];urb->dev = myuvc_udev;urb->context = NULL;urb->pipe = usb_rcvisocpipe(myuvc_udev,myuvc_bEndpointAddress);urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;urb->interval = 1;urb->transfer_buffer = myuvc_queue.urb_buffer[i];urb->transfer_dma = myuvc_queue.urb_dma[i];urb->complete = myuvc_video_complete;urb->number_of_packets = npackets;urb->transfer_buffer_length = size;for (j = 0; j < npackets; ++j) {urb->iso_frame_desc[j].offset = j * psize;urb->iso_frame_desc[j].length = psize;}}return 0;
}

2)之后写出urb卸载函数:myuvc_uninit_urbs

static void myuvc_uninit_urbs(void)
{int i;for (i = 0; i < MYUVC_URBS; ++i) {if (myuvc_queue.urb_buffer[i]){usb_buffer_free(myuvc_udev, myuvc_queue.urb_size, myuvc_queue.urb_buffer[i], myuvc_queue.urb_dma[i]);myuvc_queue.urb_buffer[i] = NULL;}if (myuvc_queue.urb[i]){usb_free_urb(myuvc_queue.urb[i]);myuvc_queue.urb[i] = NULL;}}
}

3)仿照uvc_video_complete 写出urb数据中断处理函数函数:

/* 参考: uvc_video_complete / uvc_video_decode_isoc */
static void myuvc_video_complete(struct urb *urb)
{u8 *src;u8 *dest;int ret, i;int len;int maxlen;int nbytes;struct myuvc_buffer *buf;switch (urb->status) {case 0:break;default:printk("Non-zero status (%d) in video ""completion handler.\n", urb->status);return;}/* 从irqqueue队列中取出第1个缓冲区 */if (!list_empty(&myuvc_queue.irqqueue)){buf = list_first_entry(&myuvc_queue.irqqueue, struct myuvc_buffer, irq);for (i = 0; i < urb->number_of_packets; ++i) {if (urb->iso_frame_desc[i].status < 0) {printk("USB isochronous frame "  "lost (%d).\n", urb->iso_frame_desc[i].status);continue;}src  = urb->transfer_buffer + urb->iso_frame_desc[i].offset;dest = myuvc_queue.mem + buf->buf.m.offset + buf->buf.bytesused;len = urb->iso_frame_desc[i].actual_length;  // 此次urb传输中实际子包大小/* Sanity checks:* - packet must be at least 2 bytes long* - bHeaderLength value must be at least 2 bytes (see above)    //数据头不能小于2字节* - bHeaderLength value can't be larger than the packet size.    //同时数据头不能大于整个子包packet的长度*//* 判断数据是否有效 *//* URB数据含义:* src[0] : 头部长度* src[1] : 错误状态*/if (len < 2 || src[0] < 2 || src[0] > len)continue;    //处理下一个子包/* Values for bmHeaderInfo (Video and Still Image Payload Headers, 2.4.3.3) */
#define UVC_STREAM_EOH  (1 << 7)
#define UVC_STREAM_ERR  (1 << 6)
#define UVC_STREAM_STI  (1 << 5)
#define UVC_STREAM_RES  (1 << 4)
#define UVC_STREAM_SCR  (1 << 3)
#define UVC_STREAM_PTS  (1 << 2)
#define UVC_STREAM_EOF  (1 << 1)
#define UVC_STREAM_FID  (1 << 0)/* Skip payloads marked with the error bit ("error frames"). *///忽略有错误状态的数据包if (src[1] & UVC_STREAM_ERR) {printk("Dropping payload (error bit set).\n");continue;}

uvc_video_decode_data 就是把urb中的数据复制到缓存中去,仿照复制的方式:

            /* 除去头部后的数据长度 */len -= src[0];/* 缓冲区最多还能存多少数据 */maxlen = buf->buf.length - buf->buf.bytesused;nbytes = min(len, maxlen);/* 复制数据 */memcpy(dest, src + src[0], nbytes);buf->buf.bytesused += nbytes;/* 判断一帧数据是否已经全部接收到 */if (len > maxlen) {buf->state = VIDEOBUF_DONE;}/* Mark the buffer as done if the EOF marker is set. */if (src[1] & UVC_STREAM_EOF && buf->buf.bytesused != 0) {  // src[1]的标志状态为EOF(End of Frame) 并且已经接收到的数据不为0,就表示一个urb的数据已经接收完毕!printk("Frame complete (EOF found).\n");if (len == 0)   //表明接收到了一个空的帧(urb)printk("EOF in empty payload.\n");buf->state = VIDEOBUF_DONE;}}/* 当接收完一帧数据, * 从irqqueue中删除这个缓冲区* 唤醒等待数据的进程 */if (buf->state == VIDEOBUF_DONE ||buf->state == VIDEOBUF_ERROR){list_del(&buf->irq);wake_up(&buf->wait);}}/* 再次提交URB */if ((ret = usb_submit_urb(urb, GFP_ATOMIC)) < 0) {printk("Failed to resubmit video URB (%d).\n", ret);}
}

4)完成streamon函数后开始写streamoff函数:

/* A17 停止 * 参考 : uvc_video_enable(video, 0)*/
static int myuvc_vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type t)
{struct urb *urb;unsigned int i;/* 1. kill URB */for (i = 0; i < MYUVC_URBS; ++i) {if ((urb = myuvc_queue.urb[i]) == NULL)continue;usb_kill_urb(urb);}/* 2. free URB */myuvc_uninit_urbs();/* 3. 设置VideoStreaming Interface为setting 0 */usb_set_interface(myuvc_udev, myuvc_streaming_intf, 0);return 0;
}

5)myuvc_mmap函数和myuvc_poll函数:

① myuvc_mmap函数:

/* A9 把缓存映射到APP的空间,以后APP就可以直接操作这块缓存 * 参考: uvc_v4l2_mmap*/
static int myuvc_mmap(struct file *file, struct vm_area_struct *vma)
{struct myuvc_buffer *buffer;struct page *page;unsigned long addr, start, size;unsigned int i;int ret = 0;start = vma->vm_start;size = vma->vm_end - vma->vm_start;/* 应用程序调用mmap函数时, 会传入offset参数* 根据这个offset找出指定的缓冲区*/for (i = 0; i < myuvc_queue.count; ++i) {buffer = &myuvc_queue.buffer[i];if ((buffer->buf.m.offset >> PAGE_SHIFT) == vma->vm_pgoff)break;}if (i == myuvc_queue.count || size != myuvc_queue.buf_size) {ret = -EINVAL;goto done;}/** VM_IO marks the area as being an mmaped region for I/O to a* device. It also prevents the region from being core dumped.*/vma->vm_flags |= VM_IO;/* 根据虚拟地址找到缓冲区对应的page结构体 */addr = (unsigned long)myuvc_queue.mem + buffer->buf.m.offset;while (size > 0) {page = vmalloc_to_page((void *)addr);/* 把page和APP传入的虚拟地址挂构:就是把这块page映射到这个虚拟地址上面去 */if ((ret = vm_insert_page(vma, start, page)) < 0)goto done;start += PAGE_SIZE;addr += PAGE_SIZE;size -= PAGE_SIZE;}vma->vm_ops = &myuvc_vm_ops;vma->vm_private_data = buffer;myuvc_vm_open(vma);done:return ret;
}

① myuvc_poll函数:

/* A12 APP调用POLL/select来确定缓存是否就绪(有数据) * 参考 : uvc_v4l2_poll*/
static unsigned int myuvc_poll(struct file *file, struct poll_table_struct *wait)
{struct myuvc_buffer *buf;unsigned int mask = 0;/* 从mainqueuq中取出第1个缓冲区 *//*判断它的状态, 如果未就绪, 休眠 */if (list_empty(&myuvc_queue.mainqueue)) {mask |= POLLERR;goto done;}// 从mainqueue队列中取出第一个缓冲区,这个缓冲区就是myuvc_buffer,stream表示该缓冲区是以stream节点挂入该队列的该函数的意思就是以stream节点从mainqueue队列中取出一个myuvc_buffer缓冲区buf = list_first_entry(&myuvc_queue.mainqueue, struct myuvc_buffer, stream); // 然后进行poll操作poll_wait(file, &buf->wait, wait);if (buf->state == VIDEOBUF_DONE ||buf->state == VIDEOBUF_ERROR)mask |= POLLIN | POLLRDNORM;done:return mask;
}


至此UVC驱动基本完成。

五、从零写USB摄像头驱动—实现数据传输_调试


根据返回值:-22,定位到EIO中查询:


在分配设置urb的地方加入返回值并加入打印信息:


这里的返回值是12:代表没有分配到内存:


这里的逻辑是:如果myuvc_queue.urb_buffer和myuvc_queue.urb都没有分配到,则报错。

重启之后首先设置printk的打印级别:

装载自己写的驱动之前,需要先把内核中的uvcvideo驱动卸载了:

一装载立即死机,查询内核打印信息:



在complete函数中出错,查找后修改:


装载并运行xawtv应用程序后依旧崩溃:查找原因依旧在complete函数中出错:

在complete函数中加入大量printk打印语句:


出错前的最后一句话停留在619行:

定位到代码处:


重新编译,装载,期待奇迹~~-------可惜有挂了,还是没有图像出来,但是现在至少程序没有崩溃。。。
继续调试:查看内核打印信息:

修改后重新编译装载:这次至少能看到数据了,但是还是不对

继续调试:将刚刚的内核打印语句全部去掉。


现在虽然有图像,但是图像根本不发生改变。
继续调试:


重新装载后图像终于可以基本正常显示了,只是出现了一个“段错误”:


进入代码中定位到:


猜测是这里的kfree释放内存的函数出了问题,
申请内存的时候用了

mem = vmalloc_32(nbuffers * bufsize);


释放的时候用kfree函数是否合适?在内核中的标准例程中查找vmalloc_32 ,看看释放的时候用了什么函数:

发现用了vfree函数来释放内存:

最后编译装载就没问题了。

六、从零写USB摄像头驱动—实现数据传输_设置属性

现在自己写的myuvc驱动程序还不能像内核自带的驱动程序那样通过xawtv软件设置摄像头的亮度、饱和度等信息,现在来完成这些功能。

以设置亮度为例,修改myuvc代码,让其有调整亮度的功能。

1. 先看APP以确定需要实现哪些接口

xawtv.c:grabber_scanng_vid_openv4l2_driver.open // v4l2_openget_device_capabilities(h);// 调用VIDIOC_QUERYCTRL ioctl确定是否支持某个属性/* controls */for (i = 0; i < MAX_CTRL; i++) {h->ctl[i].id = V4L2_CID_BASE+i;if (-1 == xioctl(h->fd, VIDIOC_QUERYCTRL, &h->ctl[i], EINVAL) ||(h->ctl[i].flags & V4L2_CTRL_FLAG_DISABLED))h->ctl[i].id = -1;}

怎么去获得/设置属性?
看drv0-v4l2.c (xawtv源码中)
可见这2个函数:
v4l2_read_attr : VIDIOC_G_CTRL
v4l2_write_attr : VIDIOC_S_CTRL

所以: 视频驱动里要实现3个ioctl:
VIDIOC_QUERYCTRL
VIDIOC_G_CTRL
VIDIOC_S_CTRL


要想看懂并实现这些设置属性的代码,从底层开始:

2. 硬件上怎么设置属性?


回看USB摄像头的内部结构,其中有两个接口,一个是用于传输视频数据的VideoStreaming Interface,另一个是用于控制的VideoControl Interface, 在之前编写的myuvc驱动程序中,主要是操作VideoStreaming Interface进行视频数据的读取。现在需要调节摄像头的亮度等属性,这时需要操作VideoControl Interface接口,该接口中有很多功能单元,比如CT、IT、SU、PU等等,这些单元在代码中被称作:实体entity

在UVC规范中找到process unit 的描述符,并在里面找到bmControl变量,里面的每一位都对应一种属性,不管设备是否支持这种属性,但是都列在这里,这就是UVC规范中定义的。


在驱动程序中哪里定义这些属性呢?-----UVC_ctrl.c 中:uvc_ctrls[]数组的每一项都对应一个属性

2.1 UVC规范里定义了哪些属性 : uvc_ctrl.c里数组: static struct uvc_control_info uvc_ctrls[]

static struct uvc_control_info uvc_ctrls[]{.entity       = UVC_GUID_UVC_PROCESSING, // 属于哪个entity(比如PU).selector    = PU_BRIGHTNESS_CONTROL,   // 用于亮度.index       = 0,                       // 对应Processing Unit Descriptor的bmControls[0].size      = 2,                       // 数据长度为2字节.flags       = UVC_CONTROL_SET_CUR | UVC_CONTROL_GET_RANGE| UVC_CONTROL_RESTORE,},

总结:
我们要设置亮度,需要把数据发给硬件:
首先发给 VideoControl Interface 接口 --> 发给该接口中的PU entity --> selector(PU_BRIGHTNESS_CONTROL)

2.2 我们的设备支持哪些属性

这需要去看描述符, 比如 Processing Unit Descriptor的bmControls的值为7f 14
可知BIT0为1,表示支持BRIGHTNESS,但是BIT7为0,查阅UVC规范后可以看到不支持白平衡组件。

在代码中如何实现?

uvc_ctrl.c:
uvc_ctrl_init_device    // 对于每一个entity(IT,PU,SU,OT等)list_for_each_entry(entity, &dev->entities, list) {// 取出bmControlsbmControls = ....// 计算bmControls里位值为1的个数,就是支持的属性个数ncontrols += hweight8(bmControls[i]);    // 为每一个属性分配一个struct uvc_controlentity->controls = kzalloc..(uvc_control)// 设置这些struct uvc_controlctrl = entity->controls;for (...){ctrl->entity = entity;ctrl->index = i;  // 对应哪一位}uvc_test_bit(bmControls, i) //测试bmControls 的每一位,如果为1,则表示支持该属性,并设置该属性。// 把uvc_control和uvc_control_info挂构uvc_ctrl_add_ctrl(dev, info);ctrl->info = 某个uvc_control_info数组项(同属于一个entity, index相同)

2.3 怎么去操作这些属性()

参考 uvc_query_v4l2_ctrl
uvc_find_control
找到一个uvc_control_mapping结构体:

uvc_ctrl.c里有static struct uvc_control_mapping uvc_ctrl_mappings[] {.id       = V4L2_CID_BRIGHTNESS,  // APP根据ID来找到对应的属性.name        = "Brightness",.entity       = UVC_GUID_UVC_PROCESSING,  // 属于哪了个entity(比如PU).selector  = PU_BRIGHTNESS_CONTROL,    // 用于亮度.size       = 16,                       // 数据占多少位(bit).offset        = 0,                        // 从哪位开始.v4l2_type = V4L2_CTRL_TYPE_INTEGER,   // 属性类别.data_type  = UVC_CTRL_DATA_TYPE_SIGNED,// 数据类型},uvc_control_mapping结构体 用来更加细致地描述属性uvc_query_ctrl   usb_control_msg //发起一个USB的控制传输

我们知道的是要通过 VideoControl Interface 接口将数据发给PU entity,但是发给PU的数据如何设置我们想设置的亮度,用到了数据中的哪些位?数据的类型又是什么?------这些都由uvc_control_mapping 结构体来决定。而且每一个uvc_control_info结构体都会对应一个mapping结构体。

举例说明: 要设置亮度,怎么操作?
a. 根据PU的描述符的bmControls, 从它的bit0等于1知道它支持调节亮度
b. 在uvc_control_info uvc_ctrls结构体数组中根据entity和index找到这一项:

 {.entity        = UVC_GUID_UVC_PROCESSING,.selector    = PU_BRIGHTNESS_CONTROL,.index     = 0,.size      = 2,.flags     = UVC_CONTROL_SET_CUR | UVC_CONTROL_GET_RANGE| UVC_CONTROL_RESTORE,},

知道了:这个设备支持SET_CUR, GET_CUR, GET_MIN等
要设置时,可以向PU的selector发数据, 发的数据是2字节

c. 在uvc_ctrl_mappings数组中根据ID找到对应的数组项(根据应用程序提供的ID值在数组中找对应的数组项)
从而知道了更加细致的信息,
然后使用usb_control_msg读写数据

3. 怎么写代码?

实现3个ioctl: vidioc_queryctrl/vidioc_g_ctrl/vidioc_s_ctrl
vidioc_queryctrl : 发起USB控制传输获得亮度的最小值、最大值、默认值、步进值
vidioc_s_ctrl : 把APP传入的亮度值通过USB传输发给硬件
vidioc_g_ctrl : 发起USB传输获得当前亮度值

要点:数据发给谁?发给usb_device的VideoControl Interface里面的Processing Unit 里面的PU_BRIGHTNESS_CONTROL

1)首先实现myuvc_vidioc_queryctrl 属性查询函数:

/* 参考:uvc_query_v4l2_ctrl */
int myuvc_vidioc_queryctrl (struct file *file, void *fh,struct v4l2_queryctrl *ctrl)
{__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;int ret;u8 data[2];if (ctrl->id != V4L2_CID_BRIGHTNESS)return -EINVAL;memset(ctrl, 0, sizeof *ctrl);ctrl->id   = V4L2_CID_BRIGHTNESS;ctrl->type = V4L2_CTRL_TYPE_INTEGER;strcpy(ctrl->name, "MyUVC_BRIGHTNESS");ctrl->flags = 0;pipe = usb_rcvctrlpipe(myuvc_udev, 0);type |= USB_DIR_IN;/* 发起USB传输确定这些值 */ret = usb_control_msg(myuvc_udev, pipe, GET_MIN, type, PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;ctrl->minimum = myuvc_get_le_value(data);    /* Note signedness */ret = usb_control_msg(myuvc_udev, pipe, GET_MAX, type,  PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;ctrl->maximum = myuvc_get_le_value(data); /* Note signedness */ret = usb_control_msg(myuvc_udev, pipe, GET_RES, type, PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;ctrl->step = myuvc_get_le_value(data); /* Note signedness */ret = usb_control_msg(myuvc_udev, pipe, GET_DEF, type, PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;ctrl->default_value = myuvc_get_le_value(data);    /* Note signedness */printk("Brightness: min =%d, max = %d, step = %d, default = %d\n", ctrl->minimum, ctrl->maximum, ctrl->step, ctrl->default_value);return 0;
}

2)下来是实现myuvc_vidioc_g_ctrl和myuvc_vidioc_s_ctrl函数:

/* 参考 : uvc_ctrl_get */
int myuvc_vidioc_g_ctrl (struct file *file, void *fh,struct v4l2_control *ctrl)
{__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;int ret;u8 data[2];if (ctrl->id != V4L2_CID_BRIGHTNESS)return -EINVAL;pipe = usb_rcvctrlpipe(myuvc_udev, 0);type |= USB_DIR_IN;ret = usb_control_msg(myuvc_udev, pipe, GET_CUR, type, PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;ctrl->value = myuvc_get_le_value(data);   /* Note signedness */return 0;
}/* 参考: uvc_ctrl_set/uvc_ctrl_commit */
int myuvc_vidioc_s_ctrl (struct file *file, void *fh,struct v4l2_control *ctrl)
{__u8 type = USB_TYPE_CLASS | USB_RECIP_INTERFACE;unsigned int pipe;int ret;u8 data[2];if (ctrl->id != V4L2_CID_BRIGHTNESS)return -EINVAL;myuvc_set_le_value(ctrl->value, data);pipe = usb_sndctrlpipe(myuvc_udev, 0);type |= USB_DIR_OUT;ret = usb_control_msg(myuvc_udev, pipe, SET_CUR, type, PU_BRIGHTNESS_CONTROL << 8,ProcessingUnitID  << 8 | myuvc_control_intf, data, 2, 5000);if (ret != 2)return -EIO;return 0;
}

*注意:应用程序传过来的Ctrl值储存在 struct v4l2_control ctrl 中。

七、自制USB摄像头硬件的驱动编写_修改UVC

摄像头参数:摄像头OV7740模组,CMOS/USB 双接口,DSP:ip2977;
分辨率:656 x 488,这个摄像头模块能够输出YUV和MJPEG格式的视频数据。

  1. 先在PC上把USB摄像头用起来: 修改PC LINUX的UVC驱动
  2. 在"从零写的UVC驱动"基础上修改, 让它支持这款摄像头
  3. 修改开发板上的UVC驱动, 并且在LCD上显示摄像头图像
  4. CMOS驱动,并且在LCD上显示摄像头图像

在"从零写的UVC驱动"基础上修改, 让它支持这款摄像头

(1)用lsusb命令查看硬件差异(就是读出USB摄像头的描述符后分析)

lsusb -v -d 0x1b3b:

设备描述符如下:

Bus 002 Device 005: ID 1b3b:2977
Device Descriptor:bLength                18bDescriptorType         1bcdUSB               2.00bDeviceClass          239 Miscellaneous DevicebDeviceSubClass         2 ?bDeviceProtocol         1 Interface AssociationbMaxPacketSize0        64idVendor           0x1b3b idProduct          0x2977 bcdDevice            1.0aiManufacturer           0 iProduct                0 iSerial                 0 bNumConfigurations      1Configuration Descriptor:bLength                 9bDescriptorType         2wTotalLength          492bNumInterfaces          4bConfigurationValue     1iConfiguration          0 bmAttributes         0x80(Bus Powered)MaxPower              500mAInterface Association:bLength                 8bDescriptorType        11bFirstInterface         0bInterfaceCount         2bFunctionClass         14 VideobFunctionSubClass       3 Video Interface CollectionbFunctionProtocol       0 iFunction               0 Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        0bAlternateSetting       0bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      1 Video ControlbInterfaceProtocol      0 iInterface              0 VideoControl Interface Descriptor:bLength                13bDescriptorType        36bDescriptorSubtype      1 (HEADER)bcdUVC               1.00wTotalLength           77dwClockFrequency        6.000000MHzbInCollection           1baInterfaceNr( 0)       1VideoControl Interface Descriptor:bLength                18bDescriptorType        36bDescriptorSubtype      2 (INPUT_TERMINAL)bTerminalID             1wTerminalType      0x0201 Camera SensorbAssocTerminal          0iTerminal               0 wObjectiveFocalLengthMin      1wObjectiveFocalLengthMax      3wOcularFocalLength            1bControlSize                  3bmControls           0x00002a80Iris (Absolute)Zoom (Absolute)PanTilt (Absolute)Roll (Absolute)VideoControl Interface Descriptor:bLength                11bDescriptorType        36bDescriptorSubtype      5 (PROCESSING_UNIT)Warning: Descriptor too shortbUnitID                 3bSourceID               1wMaxMultiplier          0bControlSize            2bmControls     0x0000053fBrightnessContrastHueSaturationSharpnessGammaBacklight CompensationPower Line FrequencyiProcessing             0 bmVideoStandards     0x1aNTSC - 525/60SECAM - 625/50NTSC - 625/50VideoControl Interface Descriptor:bLength                26bDescriptorType        36bDescriptorSubtype      6 (EXTENSION_UNIT)bUnitID                 4guidExtensionCode         {3aab9199-efb2-c948-8fe9-8fe3634771d0}bNumControl             8bNrPins                 1baSourceID( 0)          3bControlSize            1bmControls( 0)       0x0fiExtension              0 VideoControl Interface Descriptor:bLength                 9bDescriptorType        36bDescriptorSubtype      3 (OUTPUT_TERMINAL)bTerminalID             2wTerminalType      0x0101 USB StreamingbAssocTerminal          0bSourceID               4iTerminal               0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x81  EP 1 INbmAttributes            3Transfer Type            InterruptSynch Type               NoneUsage Type               DatawMaxPacketSize     0x0040  1x 64 bytesbInterval               6Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       0bNumEndpoints           0bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 VideoStreaming Interface Descriptor:bLength                            14bDescriptorType                    36bDescriptorSubtype                  1 (INPUT_HEADER)bNumFormats                         1wTotalLength                      121bEndPointAddress                  130bmInfo                              0bTerminalLink                       2bStillCaptureMethod                 0bTriggerSupport                     0bTriggerUsage                       1bControlSize                        1bmaControls( 0)                    11VideoStreaming Interface Descriptor:bLength                            11bDescriptorType                    36bDescriptorSubtype                  6 (FORMAT_MJPEG)bFormatIndex                        1bNumFrameDescriptors                3bFlags                              0Fixed-size samples: NobDefaultFrameIndex                  1bAspectRatioX                       0bAspectRatioY                       0bmInterlaceFlags                 0x00Interlaced stream or variable: NoFields per frame: 1 fieldsField 1 first: NoField pattern: Field 1 onlybCopyProtect                      0VideoStreaming Interface Descriptor:bLength                            30bDescriptorType                    36bDescriptorSubtype                  7 (FRAME_MJPEG)bFrameIndex                         1bmCapabilities                   0x01Still image supportedwWidth                            640wHeight                           480dwMinBitRate                  2304000dwMaxBitRate                  2304000dwMaxVideoFrameBufferSize       76800dwDefaultFrameInterval         333333bFrameIntervalType                  1dwFrameInterval( 0)            333333VideoStreaming Interface Descriptor:bLength                            30bDescriptorType                    36bDescriptorSubtype                  7 (FRAME_MJPEG)bFrameIndex                         2bmCapabilities                   0x01Still image supportedwWidth                            320wHeight                           240dwMinBitRate                   576000dwMaxBitRate                   576000dwMaxVideoFrameBufferSize       19200dwDefaultFrameInterval         333333bFrameIntervalType                  1dwFrameInterval( 0)            333333VideoStreaming Interface Descriptor:bLength                            30bDescriptorType                    36bDescriptorSubtype                  7 (FRAME_MJPEG)bFrameIndex                         3bmCapabilities                   0x01Still image supportedwWidth                            160wHeight                           120dwMinBitRate                   144000dwMaxBitRate                   144000dwMaxVideoFrameBufferSize        4800dwDefaultFrameInterval         333333bFrameIntervalType                  1dwFrameInterval( 0)            333333VideoStreaming Interface Descriptor:bLength                             6bDescriptorType                    36bDescriptorSubtype                 13 (COLORFORMAT)bColorPrimaries                     1 (BT.709,sRGB)bTransferCharacteristics            1 (BT.709)bMatrixCoefficients                 4 (SMPTE 170M (BT.601))Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       1bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0080  1x 128 bytesbInterval               1Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       2bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0100  1x 256 bytesbInterval               1Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       3bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0200  1x 512 bytesbInterval               1Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       4bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0258  1x 600 bytesbInterval               1Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       5bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0320  1x 800 bytesbInterval               1Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        1bAlternateSetting       6bNumEndpoints           1bInterfaceClass        14 VideobInterfaceSubClass      2 Video StreamingbInterfaceProtocol      0 iInterface              0 Endpoint Descriptor:bLength                 7bDescriptorType         5bEndpointAddress     0x82  EP 2 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x03bc  1x 956 bytesbInterval               1Interface Association:bLength                 8bDescriptorType        11bFirstInterface         2bInterfaceCount         2bFunctionClass          1 AudiobFunctionSubClass       0 bFunctionProtocol       0 iFunction               0 Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        2bAlternateSetting       0bNumEndpoints           0bInterfaceClass         1 AudiobInterfaceSubClass      1 Control DevicebInterfaceProtocol      0 iInterface              0 AudioControl Interface Descriptor:bLength                 9bDescriptorType        36bDescriptorSubtype      1 (HEADER)bcdADC               1.00wTotalLength           39bInCollection           1baInterfaceNr( 0)       3AudioControl Interface Descriptor:bLength                12bDescriptorType        36bDescriptorSubtype      2 (INPUT_TERMINAL)bTerminalID             1wTerminalType      0x0201 MicrophonebAssocTerminal          0bNrChannels             1wChannelConfig     0x0000iChannelNames           0 iTerminal               0 AudioControl Interface Descriptor:bLength                 9bDescriptorType        36bDescriptorSubtype      6 (FEATURE_UNIT)bUnitID                 3bSourceID               1bControlSize            2bmaControls( 0)      0x03bmaControls( 0)      0x00MuteVolumeiFeature                0 AudioControl Interface Descriptor:bLength                 9bDescriptorType        36bDescriptorSubtype      3 (OUTPUT_TERMINAL)bTerminalID             2wTerminalType      0x0101 USB StreamingbAssocTerminal          0bSourceID               3iTerminal               0 Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        3bAlternateSetting       0bNumEndpoints           0bInterfaceClass         1 AudiobInterfaceSubClass      2 StreamingbInterfaceProtocol      0 iInterface              0 Interface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        3bAlternateSetting       1bNumEndpoints           1bInterfaceClass         1 AudiobInterfaceSubClass      2 StreamingbInterfaceProtocol      0 iInterface              0 AudioStreaming Interface Descriptor:bLength                 7bDescriptorType        36bDescriptorSubtype      1 (AS_GENERAL)bTerminalLink           2bDelay                  1 frameswFormatTag              1 PCMAudioStreaming Interface Descriptor:bLength                11bDescriptorType        36bDescriptorSubtype      2 (FORMAT_TYPE)bFormatType             1 (FORMAT_TYPE_I)bNrChannels             1bSubframeSize           2bBitResolution         16bSamFreqType            1 DiscretetSamFreq[ 0]        16000Endpoint Descriptor:bLength                 9bDescriptorType         5bEndpointAddress     0x83  EP 3 INbmAttributes            1Transfer Type            IsochronousSynch Type               NoneUsage Type               DatawMaxPacketSize     0x0020  1x 32 bytesbInterval               1bRefresh                0bSynchAddress           0AudioControl Endpoint Descriptor:bLength                 7bDescriptorType        37bDescriptorSubtype      1 (EP_GENERAL)bmAttributes         0x00bLockDelayUnits         0 UndefinedwLockDelay              0 UndefinedInterface Descriptor:bLength                 9bDescriptorType         4bInterfaceNumber        3bAlternateSetting       2bNumEndpoints           1bInterfaceClass         1 AudiobInterfaceSubClass      2 StreamingbInterfaceProtocol      0 iInterface              0 AudioStreaming Interface Descriptor:bLength                 7bDescriptorType        36bDescriptorSubtype      1 (AS_GENERAL)bTerminalLink           2bDelay                  1 frameswFormatTag              1 PCMAudioStreaming Interface Descriptor:bLength                11bDescriptorType        36bDescriptorSubtype      2 (FORMAT_TYPE)bFormatType             1 (FORMAT_TYPE_I)bNrChannels             1bSubframeSize           2bBitResolution         16bSamFreqType            1 DiscretetSamFreq[ 0]         8000Endpoint Descriptor:bLength                 9bDescriptorType         5bEndpointAddress     0x83  EP 3 INbmAttributes            5Transfer Type            IsochronousSynch Type               AsynchronousUsage Type               DatawMaxPacketSize     0x0010  1x 16 bytesbInterval               1bRefresh                0bSynchAddress           0AudioControl Endpoint Descriptor:bLength                 7bDescriptorType        37bDescriptorSubtype      1 (EP_GENERAL)bmAttributes         0x00bLockDelayUnits         0 UndefinedwLockDelay              0 Undefined
Device Status:     0x0002(Bus Powered)Remote Wakeup Enabled

(2)根据这些差异修改驱动程序–全局变量参数

① 首先找到bEndpointAddress 参数,该参数是我们传输视频数据的端点地址:


接口下的bAliternateSetting参数后面再进行设置。

②在一种格式下有多重分辨率frame:









怎么去确定VideoStreaming Interface 所使用哪个Setting呢?

a. 根据分辨率确定带宽–>b. 根据这个带宽值在众多的setting里面搜索哪个setting支持这种带宽


至此全局变量修改完成。

(3)根据这些差异修改驱动程序–11个ioctl函数

① fmt相关的函数:



sizeimage:选择这种分辨率的时候,一帧数据的最大值是多少:


色彩空间 :



② 缓冲区相关的函数:没有变化


编译,装载:果然报错。
解压缩出错:

(4)调试:

①猜测是上报数据函数出了问题:


complete函数处理数据的流程框架:
MYUVC数据处理:

static void myuvc_video_complete(struct urb *urb)
{if (!list_empty(&myuvc_queue.irqqueue)){for (i = 0; i < urb->number_of_packets; ++i) {  //对于一帧数据(一个urb请求块中的数据),读取其中的多个子包// 处理头部// 复制数据// 判断是否结束// 如果结束,唤醒j进程...}// 重新提交URB
}

UVC_video.c:UVC标准驱动程序的数据处理框架流程:

static void uvc_video_complete(struct urb *urb)
{if (!list_empty(&queue->irqqueue))buf = list_first_entry(&queue->irqqueue, struct uvc_buffer,queue);//video->decode(urb, video, buf) ---> //uvc_video_decode_isocfor (i = 0; i < urb->number_of_packets; ++i) {// 处理头部 uvc_video_decode_start// 处理数据 uvc_video_decode_data// 判断是否结束 uvc_video_decode_end// 如果结束,唤醒}// 重新提交URB }


这里可以看到之前写的complete函数还不是很完善,没有对buf是空的情况作出处理。

※ 这里有个fid的概念:
在视频数据传输的过程中,数据是逐帧传输的,这个fid指的是帧的标号值(frame ID)。
根据fid判断当前帧的数据是否结束。




这里的帧表示的是一次urb传输的过程中的一个buf。



接下来就是跟之前一样拷贝数据,但是对本摄像头使用的ip2977的DSP芯片来说,还需要将官方手册中提到的修改代码加入其中:

总结:主要修改了两点

  1. 之前处理数据的时候只有当irqqueue队列不为空的时候才去处理数据;而现在即使应用程序已经占据了所有的空间导致urb的数据没办法再存入buf中了,也还是需要处理urb中的数据;
  2. 以前在处理urb数据的时候没有去注意fid参数;而现在需要考虑fid参数的作用,在USB摄像头中,视频数据是一帧一帧传输的,fid的作用就是让我们在处理视频数据的过程中做到 “帧同步” ,用fid将每一帧标上号。

修改完编译装载后依旧有问题。报错显示select timeout ,说明APP没有等到驱动程序的数据:

② 继续调试

首先看看wake_up函数有没有正常执行:
加入打印语句:




修改后重新装载:发现数据在不断的上报,但是还是解压缩出错。

有可能是JPEG数据格式不对,查阅资料后知道,所有的JPEG格式的数据都是FFD8开头,FFD9结尾的,在程序中测试一下我们视频数据格式,看符不符合JPEG格式:



重新编译装载:


说明数据格式没有问题。

回顾驱动程序的使用过程:

该函数中是否缺乏了某些东西?

查看标准uvc_queue.c中怎么实现的从队列中取出数据的操作:



这里还多了一个内存拷贝,APP传入 v4l2_buf 参数,应该将myuvc_buffer–>v4l2_buffer中的数据拷贝一份给 v4l2_buf 参数回去,这样APP才能知道当前的数据长度等等信息。


可以正常显示图像了。但是效果比较差。而且再次运行的时候会出错。
可能是在退出APP的时候清理工作没有做好,现在只是在streamoff函数中实现了清理的工作,现在将清理的工作放在myuvc_close函数中去执行:

韦东山第3期嵌入式Linux项目-视频监控-2-从零写USB摄像头驱动(UVC驱动)相关推荐

  1. 嵌入式linux进行视频监控,嵌入式Linux下的视频监控解决方案

    原标题:嵌入式Linux下的视频监控解决方案 在嵌入式工控板上接一个摄像头进行视频监控,看起来比较容易实现,但往往是"理想很丰满.现实很骨感".历经千辛万苦效果却不是很理想,本文就 ...

  2. [嵌入式Linux项目实战开发]基于QT4.7.4的音乐播放器实现与设计【2018年给力项目】

    [嵌入式Linux项目实战开发]基于QT4.7.4的音乐播放器实现与设计[2018年给力项目]是[创科之龙]团队aiku嵌入式视频教程系列制作的现有的音乐播放器. 主要功能实现: 1.新建工程,基类选 ...

  3. [嵌入式Linux项目实战开发]基于QT4.8的仓库管理系统实现功能【2019年给力项目】

    [嵌入式Linux项目实战开发]基于QT4.8的仓库管理系统实现功能[2019年给力项目] 支持导出 excel 表格 支持查看商品操作日志 支持高精度浮点运算 支持同一商品以不同价格入库 该软件已开 ...

  4. 嵌入式linux python移植过程_嵌入式linux项目开发(一)——BOA移植

    嵌入式linux项目开发(一)--BOA移植 项目目标:使用BOA.CGIC.SQLite搭建嵌入式web服务器 一.嵌入式web服务器BOA简介 在嵌入式设备的管理与交互中,基于Web方式的应用成为 ...

  5. 嵌入式linux 项目开发(一)——HTML编程

    嵌入式linux 项目开发(一)--HTML编程 本文主要讲解HTML的基础知识,主要是涉及嵌入式web服务器中数据交互HTML页面表单制作部分. 一.HTML简介 HTML是Hypertext Ma ...

  6. 韦东山第1期-学习笔记-3

    韦东山第1期视频-第9课.知识点简单,仅用于回顾.弱点补充.注意Makefile变量.函数的使用,要会分析.更改Makefile. 课表: 1.C语言指针相关 二.Makefile

  7. 韦东山第1期-学习笔记-4

    韦东山第1期视频-第6-7课.上手韦东山JZ2440开发板,配置软硬件环境.另外对几款代码查看软件进行了简要说明(常用,没有罗列) 一.课表: 二.知识图谱:

  8. 韦东山第1期-学习笔记-2

    韦东山第1期视频-第2-4课.知识点简单,仅用于回顾.弱点补充. 课表: 思维导图:

  9. ARM视频 嵌入式linux培训班视频》[DVDRip]

    ARM视频 嵌入式linux培训班视频>[DVDRip] 2008-11-15 12:09 eMule资源   http://www.verycd.com/topics/250252/ 下面是用 ...

  10. 嵌入式智能家居项目视频监控_智能化您的视频嵌入

    嵌入式智能家居项目视频监控 Video content is taking over the Internet. The trend began long ago and the most recen ...

最新文章

  1. 送书 | 2020年新一天,用这本书开启你的NLP学习之路!
  2. Android 简单天气预报
  3. jQuery编写插件
  4. java socket tomcat_在Tomcat环境下使用socket通信
  5. visibility: hidden与display:none的区别
  6. 服务器上build.xml文件乱码解决(亲测有效)
  7. SqlServer2005数据库文件损坏的拯救过程
  8. 搭建私域流量池实施落地——四大运营步骤
  9. 通过TXT文件批量生成PDF417码
  10. 金士顿U盘格式化后不能识别,0字节存储空间
  11. linux ppoe 动态ip,设置路由器时应该选择动态ip,静态ip还是pppoe拨号?
  12. 最新 Transformer 预训练模型综述!
  13. 【jzoj5289】【NOIP2017提高组A组模拟8.17】【偷笑】【数据结构】
  14. 快速了解云数据库RDS
  15. MySQL批量修改表的编码和字符集
  16. 西南科技大学学生邮箱申请
  17. java实现七牛云图片文件的上传
  18. Elasticsearch学习1 入门进阶 Linux系统下操作安装Elasticsearch Kibana 初步检索 SearchAPI Query DSL ki分词库 自定义词库
  19. unity找隐藏游戏对象Gameobject
  20. Linux下ftp下载方法--使用lftp命令

热门文章

  1. c语言观察程序流程图,程序流程图的画法
  2. java 数据结构 pdf_数据结构java版本.pdf
  3. 制作唐诗网页代码_有关于诗词的网页代码
  4. 计算机教学能力大赛实施报告模板,“现代信息技术在课堂教学中的运用”实施情况总结...
  5. js调用微信扫一扫demo_微信JSSDK调用微信扫一扫功能的方法
  6. ASP.net网页导出Excel中文乱码解决方案
  7. 开关电源设计-基础视频教程(53集全,含配套资料)-道合顺大数据Infinigo
  8. 无人机机架 无人机机架材料 无人机机架的尺寸计算 无人机机架结构图
  9. 光棍节程序员闯关秀-解密
  10. X-Scan使用教程