第三阶段应用层——2.4 视频监控—从0写USB摄像头驱动(3)-实现数据传输(完善)

视频监控—从0写USB摄像头驱动(3)-实现数据传输(完善)

  • 硬件平台:韦东山嵌入式Linxu开发板(S3C2440.v3)
  • 软件平台:运行于VMware Workstation 12 Player下UbuntuLTS16.04_x64 系统
  • 参考资料:USB_Video_Example 1.5、UVC 1.5 Class specification
  • 开发环境:Linux-4.13.0-41内核(虚拟机)、arm-linux-gcc-4.3.2工具链
  • 源码仓库:https://gitee.com/d_1254436976/Embedded-Linux-Phase-3

目录

  • 视频监控—从0写USB摄像头驱动(3)-实现数据传输(完善)
    • 一、前言
    • 二、代码编写
      • 1、设置urb请求块
        • 1.1 urb的介绍
        • 1.2 分配与初始化urb结构体
        • 1.3 将urb结构体提交给usb核心
      • 2、设置亮度属性
        • 2.1 查询当前设备是否支持调节亮度属性
        • 2.2 获得当前设备亮度属性
        • 2.3 设置当前设备亮度属性
      • 3、编写数据处理过程(不完善)


一、前言

在【2.4 视频监控—从0写USB摄像头驱动(2)-实现数据传输(初步)】中,我们已经完成了USB摄像头驱动的初步框架和初步的数据传输,现在来进行完善,设置urb请求块与亮度属性

在实际的工作和应用中,尽量使用内核带有的驱动程序,对于没有适配的驱动程序在内核的基础上进行添加,这个usb驱动程序只是学习的时候使用。


二、代码编写


1、设置urb请求块

若想要获取摄像头的视频数据,则驱动程序与USB摄像头之间需要通过urb结构体作为媒介进行信息的交互

1.1 urb的介绍

USB请求块(USB request block,URB)是USB设备驱动中用来描述与USB设备通信所用的基本载体和核心数据结构,与网络设备驱动中的sk_buff结构体类似,是USB主机与设备之间传输数据的封装

传输过程:

  1. 分配一个urb结构体,对其进行初始化;(驱动程序中进行)
  2. 将其提交给usb核心;(驱动程序中进行)
  3. USB核心对urb进行解析,将控制信息提交给主机控制器,由主机控制器负责数据到设备的传输
  4. 驱动程序只需等待,当数据回传到主机控制器后,会转发给USB核心,唤醒等待的驱动程序,由驱动程序完成剩下的工作。(驱动程序中进行)

1.2 分配与初始化urb结构体

该函数主要分为如下三步:

  1. 分配urb_buffer,存储数据的缓冲区(一个buffer可以存储多个帧数据的信息)
  2. 分配urb结构体,用来指向与描绘该缓冲区
  3. 设置urb结构体,指向与添加该缓冲区的描绘信息

修改结构体如下:

/*!
 * 存储分配的整块缓冲区
 */
typedef struct mvuvc_video_queue {
    void *mem;                          /**< 存储分配的内存 */
    int count;                          /**< 分配缓冲区个数 */
    int buf_size;                       /**< 每个缓冲区(页对齐)大小 */	
	MYUVC_BUFFER_S buffer[32];          /**< 存储每个缓冲区的信息 */

    struct urb *urb[MYMAX_PACKETS_NUM];        /**< urb_buffer的描述信息 */
    char *urb_buffer[MYMAX_PACKETS_NUM];       /**< 存储数据 */
    dma_addr_t urb_dma[MYMAX_PACKETS_NUM];     /**< urb_buffer的物理地址 */
    unsigned int urb_size;                     /**< urb_buffer的数量 */
    
    struct list_head mainqueue;         /**< mainqueue队列头结点,供APP消费用 */
    struct list_head irqqueue;          /**< irqqueue队列头结点,供底层驱动生成用*/
}MYUVC_VIDEO_QUEUE_S;

具体实现代码:

/*!
 * @brief  分配与初始化URB
 *         参考: uvc_init_video_isoc()
 * @return 0:成功   -ENOMEM:失败
 */
 static int myuvc_alloc_init_urbs(void)
{
    u16 psize;
    u32 size;
    int i, j;
    int npackets;
    struct urb *urb;
    
    psize = s_wMaxPackSie;
    size  = s_myuvc_params.dwMaxVideoFrameSize; /**< 一帧数据最大长度 */
    npackets = DIV_ROUND_UP(size, psize);      	/**< 每个urb_buffer的帧数 */

    /*!
     * urb_buffer中所有帧的大小 
     */
    size  = s_myuvc_queue.urb_size = psize * npackets;  

    if (npackets > MYMAX_PACKETS_NUM)
        npackets = MYMAX_PACKETS_NUM;
    
    /*!
     * 分配urb_buffers  :存储数据的缓冲区
     * 分配urb  结构体:指向urb_buffers
     */
	for (i = 0; i < MYUVC_URBS; ++i) {
		s_myuvc_queue.urb_buffer[i] = usb_alloc_coherent(
			s_myuvc_udev, size, GFP_KERNEL | __GFP_NOWARN, 
			   &s_myuvc_queue.urb_dma[i]);

        s_myuvc_queue.urb[i]  = usb_alloc_urb(npackets, GFP_KERNEL);

        if (!s_myuvc_queue.urb_buffer[i] || !s_myuvc_queue.urb[i]) {
            myuvc_uninit_urbs();
            return -ENOMEM;
        }
    }

    /*!
     * 设置urb
     */
    for (i = 0; i < MYUVC_URBS; ++i) {
        urb = s_myuvc_queue.urb[i];
        
        urb->dev = s_myuvc_udev;
        urb->context = NULL;
        urb->pipe = usb_rcvisocpipe(s_myuvc_udev, s_bEndpointAddress);
        urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;
        urb->interval = s_bInterval;
        urb->transfer_buffer = s_myuvc_queue.urb_buffer[i];
        urb->transfer_dma = s_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;
}

1.3 将urb结构体提交给usb核心

 	/*!
     * 3. 提交URB以接收数据
     */
    for (i = 0; i < UVC_URBS; ++i) {
		if ((ret = usb_submit_urb(s_myuvc_queue.urb[i], GFP_KERNEL)) < 0) {
			printk("Failed to submit URB %u (%d).\n", i, ret);
			myuvc_uninit_urbs();
        
			return ret;
		}
	}

2、设置亮度属性

对于一个USB摄像头,若想要获得/设置亮度属性(其他控制属性也通用),需要进行如下步骤

  1. .vidioc_reqbufs查询设备是否支持该属性,是则获得该属性的相关值(最大值、最小值、步长值、默认值)
  2. .vidioc_g_ctrl获取设备属性的当前值
  3. .vidioc_s_ctrl设置设备的属性的当前值

那么对于驱动程序,如何分辨这些“属性”呢?,以亮度属性为例子

  1. 查询手册,查看亮度属性所在VideoControl Interface的哪个单元
    对于亮度属性,在PU单元中进行设置。在PU单元的bmControls,每一位代表一个控制项,亮度Brightness属于第0位
    第三阶段应用层——2.4 视频监控—从0写USB摄像头驱动(3)-实现数据传输(完善)_第1张图片

  2. 驱动程序中如何表示这些信息
    uvc_ctrl.c中的static struct uvc_control_info uvc_ctrls[]结构体数组中:对于每一个VideoControl Interface的单元,表示为一个entity实体,在uvc_ctrls结构体中,会对该entity实体的中的控制属性selector进行相关描述
    第三阶段应用层——2.4 视频监控—从0写USB摄像头驱动(3)-实现数据传输(完善)_第2张图片

2.1 查询当前设备是否支持调节亮度属性

  1. 先去判断该设备是否支持亮度属性不支持就直接退出,返回一个标志位
  2. 支持
    2.1 清空并设置传入参数ctrl
    2.2 调用usb_control_msg(),发送控制信息获取该属性的相关值并对数据进行解析
/*!
 * @brief  发起USB控制传输获得亮度的最小值、最大值、默认值、阶梯值
 *         参考:uvc_query_v4l2_ctrl()
 */
static 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);
	strcpy(ctrl->name, "MYUVC_BRIGHTNESS");
	ctrl->id = V4L2_CID_BRIGHTNESS;
	ctrl->type = V4L2_CTRL_TYPE_INTEGER;
	ctrl->flags = 0;

    /*!
     * 从端口中接收数据,设置数据类型为输入
     */
	pipe = usb_rcvctrlpipe(s_myuvc_udev, 0);
	type = USB_DIR_IN;

    /*!
     * 发送数据到USB摄像头的VideoControl Interface的PU,获取最小亮度
     */
	ret = usb_control_msg(s_myuvc_udev, pipe, GET_MIN, type,  PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, 
			                data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO;    
    ctrl->minimum = myuvc_get_le_value(data);      /**< 解析获取的数值 */

    /*!
     * 获取最大亮度并解析
     */
    ret = usb_control_msg(s_myuvc_udev, pipe, GET_MAX, type, PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO;
    ctrl->maximum =  myuvc_get_le_value(data);

    /*!
     * 获取亮度调节阶梯值并解析
     */
    ret = usb_control_msg(s_myuvc_udev, pipe, GET_RES, type, PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO;
    ctrl->step = myuvc_get_le_value(data);

    /*!
     * 获取默认亮度并解析
     */
    ret = usb_control_msg(s_myuvc_udev, pipe, GET_DEF, type, PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO; 
		ctrl->default_value = myuvc_get_le_value(data);

    printk("Brightness: min =%d, max = %d, step = %d, default = %d\n", 
                ctrl->minimum, ctrl->maximum, ctrl->step, ctrl->default_value);
  
    return 0;
}              

2.2 获得当前设备亮度属性

  1. 判断当前需要操作的属性是否为亮度属性,不是则退出并返回错误值。
  2. 则,调用usb_control_msg(),启动USB传输,获取当前亮度并解析
/*!
 * @brief  获取当前指定的控制信息:把USB传入的亮度值通过USB传输发送给硬件
 *         参考:uvc_ctrl_get()
 */
static 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 = 0;
    u8 data[2];

    /* 只支持调节亮度 */
    if (ctrl->id != V4L2_CID_BRIGHTNESS)
        return -EINVAL;

    /*!
     * 从端口中接收数据,设置数据类型为输入
     */
    pipe = usb_rcvctrlpipe(s_myuvc_udev, 0);
	type = USB_DIR_IN;

    /*!
     * 启动USB传输,获取当前亮度并解析
     */
    ret = usb_control_msg(s_myuvc_udev, pipe, GET_CUR, type, PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO;    
    ctrl->value = myuvc_get_le_value(data);      /**< 解析获取的数值 */

    return ret;
}

2.3 设置当前设备亮度属性

  1. 判断当前需要操作的属性是否为亮度属性,不是则退出并返回错误值。
  2. 则,先对参数进行相关位的设置调用usb_control_msg(),启动USB传输,设置当前亮度
/*!
 * @brief  设置当前指定的控制信息:发起USB传输获取当前亮度值
 *         参考:uvc_ctrl_set()/uvc_ctrl_commit()
 */
static 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 = 0;
    u8 data[2];

    /* 只支持调节亮度 */
    if (ctrl->id != V4L2_CID_BRIGHTNESS)
        return -EINVAL;

    /*!
     * 设置控制信息的数据为指定位数
     */
    myuvc_set_le_value(ctrl->value, data);

    /*!
     * 发送数据到端口,设置数据类型为输出
     */
    pipe = usb_sndctrlpipe(s_myuvc_udev, 0);
	type = USB_DIR_OUT;

    /*!
     * 启动USB传输,设置亮度
     */
    ret = usb_control_msg(s_myuvc_udev, pipe, SET_CUR, type, PU_BRIGHTNESS_CONTROL << 8,
			    s_PU_ID << 8 | s_myuvc_control_intf, data, 2, MYUVC_CTRL_CONTROL_TIMEOUT);
    if (ret != 2)
        return -EIO;    

    return ret;
}

3、编写数据处理过程(不完善)

对于此数据处理过程,有不完善的地方,在下篇博文中进行修改
数据处理过程:

1、从irqqueue队列中取出第一个空缓冲区
	1.1 存储urb_buffer中的多帧数据到这缓冲区中
		1.1.1 设置数据源、数据目的地、数据长度
		1.1.2 判断每一帧的头部长度与错误信息
		1.1.3 把数据源的数据去除头部信息,按指定大小存入缓冲区中
		1.1.4 判断一帧的数据大小是否大于该缓冲区的可使用的大小,是则修改缓冲区状态位,强制传输完毕
		1.1.5 判断一帧的数据是否传输完毕,是则修改缓冲区状态位,传输完毕
		
	1.2 判断缓冲区是否接收完所有帧数据,是则从irqqueue队列中删除该缓冲区,并唤醒等待进程
	
2、再次提交urb

代码实现:

/*!
 * @brief  每次传输完数据,从irqqueu队列中取出第一空的缓冲区,把urb_buffer中的数据存到缓冲区中
 *         后再次提交URB。
 *         参考:uvc_video_complete()
 */
static void myuvc_video_complete(struct urb *urb)
{
    u8 *src;
    u8 *dest;
    int i;
    int ret;
    int len;
    int nbytes;     /**< 缓冲区中存储数据的最小字节数 */
    int maxlen;     /**< 缓冲区中实际可以存储的最大数据大小 */
    MYUVC_BUFFER_S *buf;

	switch (urb->status) {
	case 0:
		break;

	default:
		printk("Non-zero status (%d) in video "
			"completion handler.\n", urb->status);
        return ;
	}

    /* 从irqqueu队列中取出第一个空缓冲区 */
	if (!list_empty(&s_myuvc_queue.irqqueue)) {
		buf = list_first_entry(&s_myuvc_queue.irqqueue, MYUVC_BUFFER_S, irq);

        /*!
         * 每个ueb_buffer有多个帧(frame),有
         * 把符合要求的urb_buffer中的所有帧的数据存储到空缓冲区中
         */
        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;
		    }

            /* 数据源(urb_buffer的一帧) */
            src  = urb->transfer_buffer + urb->iso_frame_desc[i].offset;

            /* 数据最终去向(空的缓冲区的可存储数据)的地址 */
            dest = s_myuvc_queue.mem + buf->buf.m.offset + buf->buf.bytesused;

            /* 获取urb_buffer(一帧)的实际可使用长度 */
            len  = urb->iso_frame_desc[i].actual_length;
            
            /*!
             * src[0]:头部长度
             * 根据头部信息判断该帧是否符合要求
             */
            if (len < 2 || src[0] < 2 || src[0] > len)
                continue;

            /*!
             * src[1]:错误状态
             * 跳过有错误标记的数据包(帧)
             */
            if (src[1] & UVC_STREAM_ERR) {
                printk("Dropping payload (error bit set).\n");
                continue;
            }

            len -= src[0];  /**< 除去头部长度后的urb_buffer(一帧)数据长度 */
        	maxlen = buf->buf.length - buf->buf.bytesused;
        	nbytes = min(len, maxlen);

            /*!
             * 把urb_buffer(一帧)中的数据(除去头部信息)
             * 按照最小存储字节数nbytes存入 到 取出的空缓冲区中 
             */
            memcpy(dest, src + src[0], nbytes);
  
            buf->buf.bytesused += nbytes;   /**< 记录已使用的空间 */

            /*!
             * 判断数据是否超过最大容量
             */
            if (len > maxlen) {
                printk("Frame complete (overflow). \n");
                buf->state = VIDEOBUF_DONE;
            }
            
            /*!
             * 判断缓冲区是否接收完一帧数据
             * 如果urb_buffer(一帧)设置了EOF标记 且 缓冲区已接收到数据
             * 则将缓冲区状态标记为已完成
             */
            if (src[1] & UVC_STREAM_EOF && buf->buf.bytesused != 0) {
                printk("Frame complete (EOF found).\n");

                /* 实际urb_buffer(一帧)数据为空 */
                if (len == 0)
                    printk("EOF in empty payload.\n");

                /* 修改缓冲区的状态位 */
                buf->state = VIDEOBUF_DONE;
            }
        }
        
        /*!
         * 判断缓冲区是否接收完所有数据
         * 是,则唤醒等待数据的进程
         *     并从irqqueue中删除缓冲区
         */
        if (buf->state == UVC_BUF_STATE_DONE ||
            buf->state == UVC_BUF_STATE_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);
	}
}

你可能感兴趣的:(第三阶段应用层)