Linux攝像頭驅動2——UVC

摘自:https://hceng.cn/2018/04/22/Linux%E6%91%84%E5%83%8F%E5%A4%B4%E9%A9%B1%E5%8A%A82%E2%80%94%E2%80%94UVC/

linux攝像頭驅動學習第二篇,對USB攝像頭驅動USB video class(UVC)進行詳細分析、編寫。

這次要寫一個真正的攝像頭驅動,內容有點多。
先簡單的介紹了USB接口,瞭解Linux中USB設備描述符的意義。
然後再移植內核自帶的USB攝像頭驅動,同時也驗證了攝像頭的可用。
最後爲了學習,逐句寫一個攝像頭驅動,再總結。

1.UVC基礎

UVC是USB video class的簡寫,也就是USB接口的視頻設備。
UVC其實很好理解,就是V4L2+USB
前面的虛擬攝像頭驅動,數據的來源是自己構造的虛擬數據,現在V4L2的數據來源則是通過USB傳進來的真實攝像頭視頻數據。
除了視頻數據,攝像頭還把自己的特性(比如支持哪幾種分辨率)告訴驅動,驅動則要配置攝像頭(指定何種分辨率)。

1.1 USB基礎知識

USB分主從系統,一般而言,PC中的USB系統就是作主系統,而一般的USB鼠標、U盤則是典型的USB從系統。
爲了方便開發,USB定義了一套標準,只要是支持USB的主機,就可以支持任何一個廠商的USB鼠標、U盤,只要是被USB系統包含的設備,只要這些設備支持相應的標準,就無需重新設計驅動而直接使用。
下面簡單的列出了USB設備類型,理想情況的USB系統要對這些設備作完整的支持,設備也必須符合USB規範中的要求。

Base Class Descriptor Usage Description
00h Device Use class information in the Interface Descriptors
01h Interface Audio
02h Both Communications and CDC Control(通訊設備)
03h Interface HID (Human Interface Device)
05h Interface Physical
06h Interface Image
07h Interface Printer
08h Interface Mass Storage(存儲)
09h Device Hub
0Ah Interface CDC-Data
0Bh Interface Smart Card
0Dh Interface Content Security
0Eh Interface Video
0Fh Interface Personal Healthcare
10h Interface Audio/Video Devices
11h Device Billboard Device Class
12h Interface USB Type-C Bridge Class
DCh Both Diagnostic Device
E0h Interface Wireless Controller
EFh Both Miscellaneous
FEh Interface Application Specific
FFh Both Vendor Specific

其中UVC就是Video類。

爲了更好地描述USB設備的特徵,USB提出了設備架構的概念。
從這個角度來看,可以認爲USB設備是由一些配置接口端點
即一個USB設備可以含有一個或多個配置,在每個配置中可含有一個或多個接口,在每個接口中可含有若干個端點。

此外,驅動是綁定到USB接口上,而不是整個設備。

體現到驅動上,就是一個一個的結構體,對應設備、配置、接口、端點。

其中USB video class它是在在標準的USB協議上進行了擴展,擴展的部分稱爲Class Specific。

  • 標準的設備描述符:

    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    
     

    typedef struct Device_Descriptor

    {

    uchar bLength; //設備描述符的字節數

    uchar bDescriptorType; //設備描述符類型編號

    uint bcdUSB; //USB版本號

    uchar bDeviceClass; //USB分配的設備類

    uchar bDeviceSubClass; //USB分配的設備子類

    uchar bDeviceProtocol; //USB分配的設備協議代碼

    uchar bMaxPacketSize0; //端點0的最大包大小

    uint idVendor; //廠商編號

    uint idProduct; //產品編號

    uint bcdDevice; //設備出廠編號

    uchar iManufacturer; //設備廠商字符串索引

    uchar iProduct; //產品字符串索引

    uchar iSerialNumber; //設備序列號索引

    uchar bNumConfigurations; //可能的配置數

    }Device_Descriptor,*pDevice_Descriptor;

  • 配置描述符:

    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    
     

    typedef struct Configuration_Descriptor

    {

    uchar bLength; //配置描述符 的字節數

    uchar bDescriptorType; //配置描述符類型編號

    uint wTotalLength; //此配置返回的所有數據大小

    uchar bNumInterfaces; //此配置支持的接口數量

    uchar bConfigurationValue;//Set_Configuration命令所需要的參數

    uchar iConfiguration; //描述該配置的字符串索引

    uchar bmAttributes; //供電模式的選擇

    uchar bMaxPower; //設備從總線獲取的最大電流

    }Configuration_Descriptor,*pConfiguration_Descriptor;

  • 接口描述符:

    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    
     

    typedef struct Interface_Descriptor

    {

    uchar bLength; //接口描述符的字節數

    uchar bDescriptorType; //接口描述符的類型編號

    uchar bInterfaceNumber; //該接口的編號

    uchar bAlternateSetting; //備用的接口描述符的編號

    uchar bNumEndPoints; //該接口使用 的端點數,不包括端點0

    uchar bInterfaceClass; //接口類

    uchar bInterfaceSubClass; //接口子類

    uchar bInterfaceProtocol; //接口類協議

    uchar iInterface; //描述該接口的字符串索引值

    }Interface_Descriptor,*pInterface_Descriptor;

  • 端點描述符:

    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    
     

    typedef struct EndPoint_Descriptor

    {

    uchar bLength; //端點描述符字節數

    uchar bDescriptorType; //端點描述符類型編號

    uchar bEndpointAddress; //端點地址及輸入輸出類型

    uchar bmAtrributes; //端點的傳輸類型

    uint wMaxPacketSize; //端點收發的最大包大小

    uchar bInterval; //主機查詢端點的時間間隔

    }EndPoint_Descriptor,*pEndPoint_Descriptor;

1.2 UVC硬件模型

首先從USB官網下載標準協議相關資料:Video Class -> Video Class 1.5 document set (.zip format, size 6.58MB)
USB_Video_Example 1.5.pdf裏,可以得知硬件模型分爲兩部分:VC interfaceVS interface

VC interface用於控制,內部又分爲多個unitterminalunit用於內部處理,terminal用於內外鏈接;
VS interface用於傳輸,內部包括視頻數據傳輸的端點以及攝像頭支持的視頻格式等信息;

每個視頻有且僅有一個Vieo Control接口和可有多個Video Streaming接口;

一個接口,就相當於一個邏輯上的USB設備。
現在,想象一下當USB攝像頭插上主機,就相當於同時插上了兩個設備,可通過函數去選中其中一個設備,從而去操作它。
一個設備用於控制,比如設置亮度等;
一個設備用於獲取數據,選擇所支持的某個格式等;
這樣就基本把控制和數據分開,要控制則操作控制接口,要數據則通過數據接口。

  • VideoControl Interface用於控制,比如設置亮度。
    它內部有多個Unit/Terminal(在程序裏Unit/Terminal都稱爲entity)
    可以通過uvc_query_ctrl類似的函數來訪問:

    
     

    1

    
     

    ret = uvc_query_ctrl(dev /*哪一個USB設備*/, SET_CUR, ctrl->entity->id /*哪一個unit/terminal*/, dev->intfnum /*哪一個接口:VC interface*/, ctrl->info->selector, uvc_ctrl_data(ctrl, UVC_CTRL_DATA_CURRENT), ctrl->info->size);

  • VideoStreaming Interface用於獲得視頻數據,也可以用來選擇fromat/frame(VS可能有多種format,一個format支持多種frame,frame用來表示分辨率等信息)
    可以通過__uvc_query_ctrl類似的函數來訪問:

    
     

    1

    
     

    ret = __uvc_query_ctrl(video->dev /*哪一個USB設備*/, SET_CUR, 0, video->streaming->intfnum /*哪一個接口: VS*/, probe ? VS_PROBE_CONTROL : VS_COMMIT_CONTROL, data, size, uvc_timeout_param);

這裏的參數VS_PROBE_CONTROL只是枚舉嘗試,並不是設置,真正要設置需要使用參數VS_COMMIT_CONTROL

1.3 USB描述符

前面提到攝像頭要把自己的特性(比如支持哪幾種分辨率)告訴驅動,這個特性就是被放在USB描述符裏面。
在前面下載的USB_Video_Example 1.5.pdf文檔裏,有個UVC描述符層次結構例子:

將USB插在Ubuntu主機上,執行lsusb可以看到當前的USB設備:


 

1

2

3


 

Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Bus 002 Device 012: ID 1b3b:2977 iPassion Technology Inc.

Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

 

可根據廠家名字iPassion Technology Inc知道ID爲1b3b:2977的USB設備就是攝像頭。
再使用-v(顯示USB設備的詳細信息)和-d(僅顯示指定廠商和產品編號的設備)獲取指定設備的詳細信息:


 

1


 

lsusb -v -d 1b3b:2977

 

此時會打印出許多信息,精簡去掉詳細的數據,只留下大致框架如下:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29


 

Device Descriptor:

Configuration Descriptor:

Interface Association:

Interface Descriptor:

VideoControl Interface Descriptor:

VideoControl Interface Descriptor:

Endpoint Descriptor:

Interface Descriptor:

VideoStreaming Interface Descriptor:

VideoStreaming Interface Descriptor:

Interface Descriptor:

Endpoint Descriptor:

Interface Descriptor:

Endpoint Descriptor:

Interface Association:

Interface Descriptor:

AudioControl Interface Descriptor:

AudioControl Interface Descriptor:

Interface Descriptor:

AudioStreaming Interface Descriptor:

AudioStreaming Interface Descriptor:

Endpoint Descriptor:

AudioControl Endpoint Descriptor:

Interface Descriptor:

AudioStreaming Interface Descriptor:

AudioStreaming Interface Descriptor:

Endpoint Descriptor:

AudioControl Endpoint Descriptor:

 

可以看到設備描述符下有一個配置描述符,配置描述符下有兩個聯合接口(IAD),一個是視頻的,一個是音頻的。
同級的還有若干接口描述符,接口描述符下有若干VC、VS和端點,與前面的框架是完全對應的。

任取其中一個描述符:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15


 

VideoStreaming Interface Descriptor:

bLength 30

bDescriptorType 36

bDescriptorSubtype 7 (FRAME_MJPEG)

bFrameIndex 1

bmCapabilities 0x01

Still image supported

wWidth 640

wHeight 480

dwMinBitRate 2304000

dwMaxBitRate 2304000

dwMaxVideoFrameBufferSize 76800

dwDefaultFrameInterval 333333

bFrameIntervalType 1

dwFrameInterval( 0) 333333

 

就可以得知該攝像頭支持一種叫FRAME_MJPEG的格式,分辨率爲640*480等信息。
因此,從上面的一系列描述符,就可完全得知攝像頭的特徵,後面驅動用用到具體的特性再說明。

2.內核攝像頭驅動

對UVC進行學習,步驟大致如下:
首先分析內核自帶的UVC是如何實現的;
然後讓手裏的攝像頭工作起來,可能內核自帶的驅動可以直接用,也可能需要移植;
最後再嘗試寫一個精簡版的UVC驅動,深入理解。

2.1分析內核攝像頭驅動

在4.13.9內核中,UVC驅動在drivers/media/usb/uvc/文件夾裏,下面對uvc_driver.c進行分析。
a.構造usb_driver


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


 

struct uvc_driver {

struct usb_driver driver;

};

struct uvc_driver uvc_driver = {

.driver = {

.name = "uvcvideo",

.probe = uvc_probe,

.disconnect = uvc_disconnect,

.suspend = uvc_suspend,

.resume = uvc_resume,

.reset_resume = uvc_reset_resume,

.id_table = uvc_ids,

.supports_autosuspend = 1,

},

};


其中.id_table裏列舉了驅動支持哪些USB設備。

 

b.設置usb_driver


 

1

2

3

4

5

6

7

8

9

10


 

uvc_probe

kzalloc //分配video_device

uvc_register_chains

uvc_register_terms

uvc_register_video

vdev->v4l2_dev = &dev->vdev; //設置video_device

vdev->fops = &uvc_fops;

vdev->ioctl_ops = &uvc_ioctl_ops;

vdev->release = uvc_release;

video_register_device //註冊video_device

 

c.註冊usb_driver


 

1

2


 

uvc_init

usb_register

 

可以看到,probe()函數裏面的操作就是前面vivid驅動裏一樣的操作方式。
然後在外面加了一個usb的“殼”。

驅動的核心還是fopsioctl_ops。下面對這兩個操作函數的實現進行分析。
首先是v4l2_file_operations:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15


 

const struct v4l2_file_operations uvc_fops = {

.owner = THIS_MODULE,

.open = uvc_v4l2_open,

.release = uvc_v4l2_release,

.unlocked_ioctl = video_ioctl2,

#ifdef CONFIG_COMPAT

.compat_ioctl32 = uvc_v4l2_compat_ioctl32,

#endif

.read = uvc_v4l2_read,

.mmap = uvc_v4l2_mmap,

.poll = uvc_v4l2_poll,

#ifndef CONFIG_MMU

.get_unmapped_area = uvc_v4l2_get_unmapped_area,

#endif

};


裏面有open()release()ioctl2readmmappoll,這點和前面的虛擬驅動一樣。

 

這其中最重要的就是ioctl2,它使用video_usercopy()獲得用戶空間傳進來的參數,調用__video_do_ioctl()v4l2_ioctls[]數組裏找到對應的uvc_ioctl_ops

uvc_ioctl_ops每個函數的實現放在後面寫代碼裏,逐個講解。

UVC驅動的重點在於:

  • 對描述符的解析;
  • 屬性的控制: 通過VideoControl Interface來設置;
  • 格式的選擇:通過VideoStreaming Interface來設置;
  • 數據的獲得:通過VideoStreaming Interface的URB來獲得;

2.2移植內核攝像頭驅動

我手裏使用的是百問網提供的二合一攝像頭,它既有CMOS接口,也有USB接口。
使用USB接口時,上面有一個DSP芯片,可以將原始的YUV數據轉換成MJPEG的壓縮數據。

它基本是符合UVC規範的,但有些小差別,廠家提供的文檔裏面有說明,按着說明修改即可。
主要添加了usb_device_id和修改了數據的處理。詳細參考補丁,修改後的代碼在Github
編譯完成後,先加載內核自帶的uvcvideo及依賴,然後移除內核自帶的驅動,安裝修改後的驅動,運行xawtv應用程序:


 

1

2

3

4


 

sudo modprobe uvcvideo

sudo rmmod uvcvideo

sudo insmod uvcvideo.ko

xawtv -noalsa

 

  • 效果:

3.編寫UVC驅動

UVC的驅動有點長,我儘量根據功能將其分解若干部分,逐一編寫。
當USB插上主機,就會產生兩個接口(VC和VS),然後獲取到USB描述符並解析,從而設置攝像頭(比如分辨率、格式);然後分配緩衝區,啓動攝像頭,便從USB得到攝像頭採集數據,保存到緩衝區供應用程序使用。
整個流程就大致這樣,因此將其分爲了6個部分進行編寫。

  • 1.註冊(USB和Video)
  • 2.數據格式設置相關
  • 3.緩衝區操作相關
  • 4.屬性相關(以亮度控制爲例)
  • 5.URB
  • 6.啓動/停止
  • 7.其它操作函數(mmap和poll)
  • 8.測試/效果

3.1 註冊(USB和Video)

在入口函數先“套”一個USB驅動的框架,首先分配一個usb_driver


 

1

2

3

4

5

6


 

static struct usb_driver my_uvc_driver = {

.name = "my_uvc",

.probe = my_uvc_probe,

.disconnect = my_uvc_disconnect,

.id_table = my_uvc_ids,

};


其中的id_table只包含我們所需的VC和VS,這樣攝像頭的Audio接口,就不會被識別:


 

1

2

3

4

5

6

7


 

static struct usb_device_id my_uvc_ids[] =

{

/* Generic USB Video Class */

{ USB_INTERFACE_INFO(USB_CLASS_VIDEO, 1, 0) }, /* VideoControl Interface */

{ USB_INTERFACE_INFO(USB_CLASS_VIDEO, 2, 0) }, /* VideoStreaming Interface */

{}

};


這裏USB_INTERFACE_INFO宏參數分別是前面接口描述符裏的bInterfaceClass(接口類),bInterfaceSubClass(接口子類),bInterfaceProtocol(接口類協議)。


 

1

2

3

4

5


 

#define USB_INTERFACE_INFO(cl, sc, pr) \

.match_flags = USB_DEVICE_ID_MATCH_INT_INFO, \

.bInterfaceClass = (cl), \

.bInterfaceSubClass = (sc), \

.bInterfaceProtocol = (pr)


這裏傳入的第一個參數都是video類,第二個分別是VC和VS,第三個參數都是無協議。這些設置的依據來自於攝像頭的USB描述符:


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

Interface Descriptor:

……

bInterfaceClass 14 Video

bInterfaceSubClass 1 Video Control

bInterfaceProtocol 0

……

Interface Descriptor:

……

bInterfaceClass 14 Video

bInterfaceSubClass 2 Video Streaming

bInterfaceProtocol 0

……

 

這裏驅動的usb_device_id和攝像頭提供的一旦匹配後,就會調用probe()函數,這裏兩個接口,就會調用兩次。

probe()函數裏,需要先得到usb_device,用於對usb設備的操作,以及分別得到兩個接口的編號,用於後面分別調用每個接口。
再在probe()函數裏做常規的分配、設置、註冊video_device


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50


 

static int my_uvc_probe(struct usb_interface *intf, const struct usb_device_id *id)

{

static int cnt = 0;

int ret;

printk("enter %s\n", __func__);

//usb_device_id會使probe()調用兩次,然而創建video_device只需要一次

cnt++;

my_uvc_udev = interface_to_usbdev(intf); //獲取usb設備

if (cnt == 1) //獲取編號

my_uvc_control_intf = intf->cur_altsetting->desc.bInterfaceNumber;

else if (cnt == 2)

my_uvc_streaming_intf = intf->cur_altsetting->desc.bInterfaceNumber;

if (cnt == 2)

{

/* 1.分配一個video_device結構體 */

my_uvc_vdev = video_device_alloc();

if (NULL == my_uvc_vdev)

{

printk("Faile to alloc video device (%d)\n", ret);

return -ENOMEM;

}

/* 2.設置 */

my_uvc_vdev->release = my_uvc_release;

my_uvc_vdev->fops = &my_uvc_fops;

my_uvc_vdev->ioctl_ops = &my_uvc_ioctl_ops;

my_uvc_vdev->v4l2_dev = &v4l2_dev;

/* 3. 註冊 */

ret = video_register_device(my_uvc_vdev, VFL_TYPE_GRABBER, -1);

if (ret < 0)

{

printk("Faile to video_register_device.\n");

return ret;

}

else

printk("video_register_device ok.\n");

/* 爲了確定帶寬,使用哪一個setting */

my_uvc_try_streaming_params(&my_uvc_params); //測試參數

my_uvc_get_streaming_params(&my_uvc_params); //取出參數

my_uvc_set_streaming_params(&my_uvc_params); //設置參數

}

return 0;

}

 

對應的disconnect也會被調用兩次,但只做一次釋放操作:


 

1

2

3

4

5

6

7

8

9

10

11

12

13


 

static void my_uvc_disconnect(struct usb_interface *intf)

{

static int cnt = 0;

printk("enter %s\n", __func__);

cnt++;

if (cnt == 2)

{

video_unregister_device(my_uvc_vdev);

video_device_release(my_uvc_vdev);

}

}

 

現在,就完成了USB設備Video設備的註冊。
且爲Video設備綁定了操作函數,後續的工作就是完善操作函數。

3.2 數據格式設置相關

前面Video設備綁定了fops,這裏主要有五個操作函數:


 

1

2

3

4

5

6

7

8

9


 

static const struct v4l2_file_operations my_uvc_fops =

{

.owner = THIS_MODULE,

.open = my_uvc_open,

.release = my_uvc_close,

.mmap = my_uvc_mmap,

.unlocked_ioctl = video_ioctl2, /* V4L2 ioctl handler */

.poll = my_uvc_poll,

};

 

open()close()沒什麼好說的,常規操作:


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15


 

static int my_uvc_open(struct file *file)

{

printk("enter %s\n", __func__);

return 0;

}

static int my_uvc_close(struct file *file)

{

printk("enter %s\n", __func__);

my_uvc_vidioc_streamoff(NULL, NULL, 0);

return 0;

}


關閉的時候,順手調用vidioc_streamoff關閉數據採集。

 

mmap()poll()涉及buf的操作,後面再講。
先講ioctl裏面幾個稍微簡單點的操作函數。

首先是vidioc_querycap(),用於表明本設備是一個攝像頭設備。
需要對v4l2_capability結構體的driver命名,card命名,version指定版本號,capabilities指定支持的功能,device_caps通過節點訪問的功能。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16


 

static int my_uvc_vidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap)

{

struct video_device *vdev = video_devdata(file);

printk("enter %s\n", __func__);

strlcpy(cap->driver, "my_uvc_video", sizeof(cap->driver));

strlcpy(cap->card, vdev->name, sizeof(cap->card));

cap->version = 4;

cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;

cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;

return 0;

}

 


然後是vidioc_enum_fmt_vid_cap(),用於列舉攝像頭支持的格式。
從USB攝像頭的設備描述符可知,本攝像頭只支持一種MJPEG格式,通過index來限定只接受一種格式。
需要設置v4l2_fmtdesc結構體的description(格式名字)、pixelformat(格式對應的像素格式)和type(v4l2_buf_type)。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14


 

static int my_uvc_vidioc_enum_fmt_vid_cap(struct file *file, void *priv, struct v4l2_fmtdesc *f)

{

printk("enter %s\n", __func__);

/* 根據攝像頭的設備描述符可知,只支持一種格式:VS_FORMAT_MJPEG */

if(f->index >= 1)

return -EINVAL;

strcpy(f->description, MY_UVC_FMT); //支持格式

f->pixelformat = V4L2_PIX_FMT_MJPEG;

f->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

return 0;

}

 


之後是獲取攝像頭數據格式vidioc_g_fmt_vid_cap()操作函數。
這個比較簡單,直接返回my_uvc_format即可。


 

1

2

3

4

5

6

7

8


 

static int my_uvc_vidioc_g_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)

{

printk("enter %s\n", __func__);

memcpy(f, &my_uvc_format, sizeof(my_uvc_format));

return 0;

}

 


再是vidioc_try_fmt_vid_cap(),用於嘗試設置攝像頭數據的格式。
先判斷傳入的v4l2_format結構體裏的typepixelformat是不是正確的格式。
再設置v4l2_pix_format結構體的width(寬)、height(高)和filed(數據掃描方式:不交錯)。
以及sizeimage(每幀圖像大小),這裏的值大小的確定是通過probe()裏打印的dwMaxVideoFrameSize值,這裏每幀的理論大小是width*height=320*240=76800小於dwMaxVideoFrameSize=77312,估計最大每幀圖像還會包含其它數據。
大多數網絡攝像頭的colorspace(顏色空間)都是V4L2_COLORSPACE_SRGB
priv(私有數據)由pixelformat決定。
這裏的所有設置的值,理論上都來自對USB設備描述符的解析,這裏簡化了代碼解析的過程,直接賦值,實際開發中爲了適配多個攝像頭,應該讀取後解析。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23


 

static int my_uvc_vidioc_try_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)

{

printk("enter %s\n", __func__);

if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE || f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG)

return -EINVAL;

/* 調整format的width, height */

f->fmt.pix.width = my_uvc_wWidth; //設備描述符裏支持的分辨率:640x480,320x240,160x120

f->fmt.pix.height = my_uvc_wHeight;

f->fmt.pix.field = V4L2_FIELD_NONE;

/* 計算bytesperline, sizeimage */

//bBitsPerPixel = my_uvc_bBitsPerPixel; //lsusb:bBitsPerPixel

//f->fmt.pix.bytesperline = (f->fmt.pix.width * bBitsPerPixel) >> 3;

f->fmt.pix.sizeimage = dwMaxVideoFrameSize; //f->fmt.pix.height * f->fmt.pix.bytesperline;

f->fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;

f->fmt.pix.priv = 0; /* private data, depends on pixelformat */

return 0;

}

 


最後是設置攝像頭的數據的格式vidioc_s_fmt_vid_cap()
先參數設置傳入的v4l2_format,如果不支持返回錯誤。支持的話,直接賦值給my_uvc_format


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14


 

static int my_uvc_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)

{

int ret;

printk("enter %s\n", __func__);

ret = my_uvc_vidioc_try_fmt_vid_cap(file, NULL, f);

if (ret < 0)

return ret;

memcpy(&my_uvc_format, f, sizeof(my_uvc_format));

return 0;

}


至此,就完成了對攝像頭數據格式my_uvc_format的設置。
應用層就可以對攝像頭數據格式進行操作,比如選擇何種數據格式、何種分辨率等,當然,這裏的驅動沒有提供選擇,全都直接賦值了。

 

3.3 緩衝區操作相關

buf操作是一個難點,容易出問題的地方。
首先是申請緩衝區vidioc_reqbufs(),應用層ioctl調用此函數,讓其分配若干個buf,應用層後面將從這些buf讀取視頻數據。
驅動先從傳入的v4l2_requestbuffers結構體獲得count(buf數量),每個buf的大小是前面my_uvc_formatsizeimage(每幀圖像大小),且長度頁對齊。

  • PAGE_ALIGN
    PAGE_ALIGN在內核裏作用是將數據以4K頁大小上界對齊。
    舉個例子:
    假如傳入的數據大小是4000字節,那麼結果得到4096字節;
    假如傳入的數據大小是4096字節,那麼結果得到4096字節;
    假如傳入的數據大小是5000字節,那麼結果得到8192字節;

源碼:
#define PAGE_SIZE 4096
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_ALIGN(x) ((x + PAGE_SIZE - 1) & PAGE_MASK)

實質:PAGE_ALIGN(x) = ((x + 4095) & (~4095))

然後再判斷my_uvc_queue結構體裏的mem(內存地址)是否爲空,非空的話說明原來已經分配了buf,需要先釋放內存、清空my_uvc_queue
如果傳入需要的buf數量爲0,則表明不需要分配,直接退出。
然後就分配buf,將所有buf作爲一個整體一次性分配,大小也就是nbuffers * bufsize,如果分配失敗,減小buf數量,再嘗試。
現在就有了一整塊buf,對應的起始地址是mem,再清空my_uvc_queue進行初始化。
再初始化兩個隊列(雙向鏈表),mainqueue用於供應用層讀取數據用,irqqueue用於供驅動產生數據用。
再依次設置每個buf的v4l2_buffer結構體的index(索引)、m.offset(偏移)、length(大小)、type(類型)、sequence(序列計數)、field(掃描方式)、memory(內存類型)、flags(標誌),再設置my_uvc_bufferstate(狀態)和初始化等待隊列wait
最後再設置my_uvc_q,記錄buf首地址、數量和大小。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55


 

/* APP調用該ioctl讓驅動程序分配若干個buf, APP將從這些buf中讀到視頻數據 */

static int my_uvc_vidioc_reqbufs(struct file *file, void *priv, struct v4l2_requestbuffers *p)

{

unsigned int i;

void *mem = NULL;

int nbuffers = p->count; //buf數量

int bufsize = PAGE_ALIGN(my_uvc_format.fmt.pix.sizeimage); //buf大小,且長度頁對齊

printk("enter %s\n", __func__);

if (my_uvc_q.mem) //如果原來分配了buf,先釋放原來的buf

{

vfree(my_uvc_q.mem);

memset(&my_uvc_q, 0, sizeof(my_uvc_q));

my_uvc_q.mem = NULL;

}

if (nbuffers == 0) //沒有需要分配的,直接退出

return 0;

for (; nbuffers > 0; --nbuffers) //依次減少buf數量,直到分配成功

{

mem = vmalloc_32(nbuffers * bufsize); //這些buf是一次性作爲一個整體來分配的

if (mem != NULL)

break;

}

if (mem == NULL)

return -ENOMEM;

memset(&my_uvc_q, 0, sizeof(my_uvc_q)); //清空my_uvc_q,初始化

INIT_LIST_HEAD(&my_uvc_q.mainqueue); //初始化兩個隊列,my_uvc_vidioc_qbuf

INIT_LIST_HEAD(&my_uvc_q.irqqueue);

for (i = 0; i < nbuffers; ++i)

{

my_uvc_q.buffer[i].buf.index = i; //索引

my_uvc_q.buffer[i].buf.m.offset = i * bufsize; //偏移

my_uvc_q.buffer[i].buf.length = my_uvc_format.fmt.pix.sizeimage; //原始大小;實測PAGE_ALIGN對齊,也沒問題

my_uvc_q.buffer[i].buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //視頻捕獲設備

my_uvc_q.buffer[i].buf.sequence = 0;

my_uvc_q.buffer[i].buf.field = V4L2_FIELD_NONE;

my_uvc_q.buffer[i].buf.memory = V4L2_MEMORY_MMAP;

my_uvc_q.buffer[i].buf.flags = 0;

my_uvc_q.buffer[i].state = VIDEOBUF_IDLE; //分配完更新狀態爲空閒

init_waitqueue_head(&my_uvc_q.buffer[i].wait); //初始化一個等待隊列

}

my_uvc_q.mem = mem;

my_uvc_q.count = nbuffers;

my_uvc_q.buf_size = bufsize;

return nbuffers;

}


這樣,我們就得到一個my_uvc_queue結構體,這個結構體裏面的my_uvc_buffer結構體數組,存放了每個buf的信息。示意如下:

 


接下來是vidioc_querybuf(),用於查詢buf,獲得buf的地址信息等。
先判斷傳入的v4l2_buffer結構體中的index是否超出了buf數量範圍。
然後將my_uvc_q中的對應的v4l2_buffer傳給傳入的v4l2_buf
再判斷my_uvc_buffer中的vma_use_count是否表示被mmap(),對應修改標準位。
最後再將uvc的state flags轉換成V4L2的state flags,其實它們的值都是一樣的,


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38


 

/* 查詢緩存狀態, 比如地址信息(APP可以用mmap進行映射) */

static int my_uvc_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)

{

int ret = 0;

printk("enter %s\n", __func__);

if (v4l2_buf->index >= my_uvc_q.count)

{

ret = -EINVAL;

goto done;

}

memcpy(v4l2_buf, &my_uvc_q.buffer[v4l2_buf->index].buf, sizeof(*v4l2_buf));

if (my_uvc_q.buffer[v4l2_buf->index].vma_use_count) //更新flags

v4l2_buf->flags |= V4L2_BUF_FLAG_MAPPED;

#if 0

switch (my_uvc_q.buffer[v4l2_buf->index].state) //將uvc flags轉換成V4L2 flags

{

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;

}

#endif

done:

return ret;

}


這樣,就將對應的v4l2_buffer相關信息傳給了應用層,應用層就通過此函數查詢各個buf信息。

 


vidioc_qbuf()是將前面的buf放入到隊列中。
首先是判斷傳入的v4l2_buffer的類型、內存種類、節點是否超過最大數量和my_uvc_qmy_uvc_buffer狀態是否處於空閒。
然後修改my_uvc_qmy_uvc_buffer狀態爲處於隊列中VIDEOBUF_QUEUED,初始化v4l2_buffer中的bytesused(緩衝區中數據的大小)爲0。
然後把對應buf的streamirq分別加到隊列mainqueue和隊列irqqueue尾部。

  • 隊列mainqueue:供應用層使用,當隊列中緩衝區有數據時, 應用層從mainqueue隊列中取出數據;
  • 隊列irqqueue:供產生數據的函數使用,當採集到數據時,從irqqueue隊列中取出首個緩衝區,存入數據;
    
     

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    
     

    /* 把傳入的緩衝區放入隊列, 底層的硬件操作函數將會把數據放入這個隊列的緩存 */

    static int my_uvc_vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)

    {

    printk("enter %s\n", __func__);

    /* 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 >= my_uvc_q.count)

    return -EINVAL;

    if (my_uvc_q.buffer[v4l2_buf->index].state != VIDEOBUF_IDLE)

    return -EINVAL;

    /* 1. 修改狀態 */

    my_uvc_q.buffer[v4l2_buf->index].state = VIDEOBUF_QUEUED;

    my_uvc_q.buffer[v4l2_buf->index].buf.bytesused = 0;

    /* 2. 放入2個隊列 */

    //隊列1: 供應用層使用

    //當隊列中緩衝區有數據時, 應用層從mainqueue隊列中取出數據

    list_add_tail(&my_uvc_q.buffer[v4l2_buf->index].stream, &my_uvc_q.mainqueue);

    //隊列2: 供產生數據的函數使用

    //當採集到數據時,從irqqueue隊列中取出首個緩衝區,存入數據

    list_add_tail(&my_uvc_q.buffer[v4l2_buf->index].irq, &my_uvc_q.irqqueue);

    return 0;

    }

    通過此函數,就將傳入的v4l2_buffer放在了兩個隊列中。

最後是將數據從隊列取出vidioc_dqbuf()
這裏是應用層想得到數據,因此是從mainqueue隊列獲取。
首先判斷mainqueue是否是空隊列,然後以my_uvc_q.mainqueue作爲頭節點,搜索my_uvc_buffer結構體中的stream,得到隊列中第一個my_uvc_buffer的地址。
再把my_uvc_bufferstate(狀態)改爲VIDEOBUF_IDLE(空閒)。、
再將該節點從隊列刪除,最後返回v4l2_buf


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31


 

/* APP通過poll/select確定有數據後,把buf從mainqueue隊列中取出來 */

static int my_uvc_vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *v4l2_buf)

{

struct my_uvc_buffer *get_buf;

printk("enter %s\n", __func__);

if (list_empty(&my_uvc_q.mainqueue))

return -EINVAL;

get_buf = list_first_entry(&my_uvc_q.mainqueue, struct my_uvc_buffer, stream); //取出buf

switch (get_buf->state) //修改狀態

{

case VIDEOBUF_ERROR:

return -EIO;

case VIDEOBUF_DONE:

get_buf->state = VIDEOBUF_IDLE;

break;

case VIDEOBUF_IDLE:

case VIDEOBUF_QUEUED:

case VIDEOBUF_ACTIVE:

default:

return -EINVAL;

}

list_del(&get_buf->stream); //從隊列刪除

memcpy(v4l2_buf, &get_buf->buf, sizeof *v4l2_buf); //複製返回數據

return 0;

}

 

至此,對buf的基本操作就完成了,包括buf的申請、查詢、放入/取出到隊列。
其中,隊列的變化如下:

初始狀態,隊列mainqueue和隊列irqqueue串連起了傳進來的buf。
產生數據的時候,buf[0]裝入數據,且斷開與隊列irqqueue的連接,此時buf[1]是隊列irqqueue的第一個節點。
取出數據的時候,buf[0]取出數據,且斷開與隊列mainqueue的連接,此時buf[1]是隊列mainqueue的第一個節點。
待數據處理完成,buf[0]將被再次放入隊列,此時在隊列尾部。
周而復始完成放入、取出隊列。

3.4 屬性相關(以亮度控制爲例)

接下來是操作攝像頭屬性,以亮度控制爲例,查詢、獲取、設置攝像頭的亮度屬性。
從前面的UVC硬件模型中可以得知,VC interface是用於控制攝像頭的,其中PU單元用於屬性的控制。
UVC 1.5 Class specification.pdf文檔裏,找到Processing Unit Descriptor,其中的bmControls表示攝像頭支持屬性的含義:


 

1

2

3

4

5

6

7

8

9

10

11

12


 

A bit set to 1 indicates that the mentioned Control is supported for the video stream.

D0: Brightness

D1: Contrast

D2: Hue

D3: Saturation

D4: Sharpness

D5: Gamma

D6: White Balance Temperature

D7: White Balance Component

D8: Backlight Compensation

D9: Gain

……

 

再找到本攝像頭USB描述符中VC interface DescriptorPROCESSING_UNIT中的bmControls,其值是0x0000053f,對應支持的屬性也就是其下面的幾個屬性,Brightness(亮度)控制是支持的。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18


 

VideoControl Interface Descriptor:

bLength 11

bDescriptorType 36

bDescriptorSubtype 5 (PROCESSING_UNIT)

Warning: Descriptor too short

bUnitID 3

bSourceID 1

wMaxMultiplier 0

bControlSize 2

bmControls 0x0000053f

Brightness

Contrast

Hue

Saturation

Sharpness

Gamma

Backlight Compensation

Power Line Frequency

 

在代碼中,UVC規範定義的屬性在uvc_ctrl.c裏的一個uvc_control_info結構體類型的vc_ctrls數組裏。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18


 

{

.entity = UVC_GUID_UVC_PROCESSING, //屬於哪個entity(比如PU)

.selector = UVC_PU_BRIGHTNESS_CONTROL, //用於亮度

.index = 0, //對應Processing Unit Descriptor的bmControls[0]

.size = 2, //數據長度爲2字節

.flags = UVC_CTRL_FLAG_SET_CUR //支持SET_CUR、GET_RANGE(GET_CUR、GET_MIN、GET_MAX)等

| UVC_CTRL_FLAG_GET_RANGE

| UVC_CTRL_FLAG_RESTORE,

},

{

.entity = UVC_GUID_UVC_PROCESSING,

.selector = UVC_PU_CONTRAST_CONTROL,

.index = 1,

.size = 2,

.flags = UVC_CTRL_FLAG_SET_CUR

| UVC_CTRL_FLAG_GET_RANGE

| UVC_CTRL_FLAG_RESTORE,

},


現在,文檔、硬件、代碼三者都找到了對應。

 

此外,uvc_control_mapping結構體類型的uvc_ctrl_mappings數組更加細緻地描述屬性。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20


 

{

.id = V4L2_CID_BRIGHTNESS, //應用層根據ID來找到對應屬性

.name = "Brightness", //名字

.entity = UVC_GUID_UVC_PROCESSING, //屬於哪了個entity(比如PU)

.selector = UVC_PU_BRIGHTNESS_CONTROL, //用於亮度控制

.size = 16, //數據佔多少位

.offset = 0, //從哪位開始

.v4l2_type = V4L2_CTRL_TYPE_INTEGER, //屬性類別(整數)

.data_type = UVC_CTRL_DATA_TYPE_SIGNED, //數據類型(有符號整數)

},

{

.id = V4L2_CID_CONTRAST,

.name = "Contrast",

.entity = UVC_GUID_UVC_PROCESSING,

.selector = UVC_PU_CONTRAST_CONTROL,

.size = 16,

.offset = 0,

.v4l2_type = V4L2_CTRL_TYPE_INTEGER,

.data_type = UVC_CTRL_DATA_TYPE_UNSIGNED,

},

 

因此,屬性控制的準備工作有:

1.獲取攝像頭的設備描述符,根據PU的描述符的bmControls,得知它支持哪些屬性;
2.從uvc_ctrls數組中根據entityindex找到對應屬性,得知其支持的操作(SET_CUR、GET_CUR等);
3.從uvc_ctrl_mappings數組中根據ID找到對應屬性,得知其更加詳細信息(整數等);


首先是查詢屬性vidioc_queryctrl(),應用層傳入一個v4l2_queryctrl結構體,驅動設置其參數返回。
需要設置的參數有id(ID)、type(類型)、name(名字)、flags(標誌)、minimum(最小值)、maximum(最大值)、step(步長)、default_value(典型值),其中前面幾個是根據前面的準備工作得知的值,直接賦值,後面的幾個需要使用usb_control_msg()函數向攝像頭髮起USB傳輸,獲取對應值。

  • usb_control_msg()
    功能:發送一個簡單的控制消息到指定的端點,並等待消息完成或超時;
    參數:
      dev:指向控制消息所發送的目標USB設備(usb_device)的指針; <這裏是在probe()裏獲取的my_uvc_udev>
      pipe:控制消息所發送的目標USB設備的特定端點,調用usb_sndctrlpipe(把指定USB設備的指定端點設置爲一個控制OUT端點)或usb_rcvctrlpipe(把指定USB設備的指定端點設置爲一個控制IN端點)來創建的; <這裏把my_uvc_udev設置爲接收端點>
      request:控制消息的USB請求值; <這裏分別是需要的GET_MIN、GET_MAX、GET_RES、GET_DEF>
      requesttype:控制消息的USB請求類型值; <這裏爲USB_TYPE_CLASS(1<<5)、usb_recip_interface(1<<0)、usb_dir_in(1<<7)>
        D7:數據的傳輸方向:0表示從主機到設備;1表示從設備到主機;
        D6~5:命令的類型:0表示標準命令;1表示類命令;2表示廠商提供的命令;3保留;
        D4~0:接收對象:0表示設備; 1表示接口;2表示端點;3表示其他;
      value:控制消息的USB消息值; <這裏是PU亮度控制>
      index:控制消息的USB消息索引值;<這裏是PU對應的ID和控制接口>
      data:指向要發送/接收的數據的指針; <這裏是接收數據>
      size:data參數所指緩衝區的大小; <這裏是兩字節,bControlSize=2>
      timeout:以msecs爲單位,期望等待的超時時間,如果爲0,該函數將一直等待消息結束以的時間;<這裏是5s>
    返回值:
      成功返回接收/發送的字節數,否則返回負的錯誤值;

通過usb_control_msg()獲得的data還需要調用my_uvc_get_le_value()進行轉換成value


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46


 

int my_uvc_vidioc_queryctrl (struct file *file, void *fh, struct v4l2_queryctrl *ctrl)

{

unsigned char data[2];

if (ctrl->id != V4L2_CID_BRIGHTNESS) //這裏只操作控制亮度的v4l2_queryctrl

return -EINVAL;

memset(ctrl, 0, sizeof * ctrl); //初始化,清空

ctrl->id = V4L2_CID_BRIGHTNESS; //設置ID

ctrl->type = V4L2_CTRL_TYPE_INTEGER; //設置屬性類別(整數)

strcpy(ctrl->name, "MY_UVC_BRIGHTNESS"); //設置名字

ctrl->flags = 0; //默認支持設置等

/* 發起USB傳輸,從攝像頭獲取這些值 */

//設置最小值

if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_MIN, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

ctrl->minimum = my_uvc_get_le_value(data);

//設置最大值

if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_MAX, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

ctrl->maximum = my_uvc_get_le_value(data);

//設置步長

if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_RES, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

ctrl->step = my_uvc_get_le_value(data);

//設置典型值

if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_DEF, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

ctrl->default_value = my_uvc_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;

}


之後是vidioc_g_ctrl()(獲得屬性)和vidioc_s_ctrl()(設置屬性),操作和前面差不多,都是通過usb_control_msg()函數建立控制消息,從而發送/接收亮度數據。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33


 

int my_uvc_vidioc_g_ctrl (struct file *file, void *fh, struct v4l2_control *ctrl)

{

unsigned char data[2];

if (ctrl->id != V4L2_CID_BRIGHTNESS)

return -EINVAL;

if(2 != usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

ctrl->value = my_uvc_get_le_value(data);

return 0;

}

int my_uvc_vidioc_s_ctrl (struct file *file, void *fh, struct v4l2_control *ctrl)

{

unsigned char data[2];

if (ctrl->id != V4L2_CID_BRIGHTNESS)

return -EINVAL;

my_uvc_set_le_value(ctrl->value, data);

if(2 != usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),

SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,

PU_BRIGHTNESS_CONTROL << 8, my_uvc_bUnitID << 8 | my_uvc_control_intf, data, 2, 5000))

return -EIO;

return 0;

}


至此,對屬性進行操作,比如對攝像頭亮度控制就完成了,其它的屬性控制類似。

 

3.5 URB

USB Request Block(URB)是Linux內核中,USB驅動實現的一個數據結構,用於組織每一次的USB設備驅動的數據傳輸請求。
也就是說,將USB傳輸相關信息放到URB這個結構體中,發送給USB核心,USB核心解析該結構體,從而進行所需數據/控制相關操作。

所需的操作大致有三步:
1.分配usb_buffers,作爲數據的緩衝區;
2.分配URB;
3.設置URB;

  • 爲什麼要usb_buffer?
    從這個角度想:前面的my_uvc_buffer作爲內核與用於空間的buf進行交互,這裏的urb_buffer作爲內核與USB設備的buf進行交互,最後類似urb_buffer = my_uvc_buffer,就實現了USB設備的數據傳到用戶層了。

首先,USB每次傳輸的數據大小,是可變的,根據外部設備的能力決定,比如外部設備支持一次傳輸100、200或800字節數據,每次傳輸稱爲Packet(包);
其次,USB每次需要傳輸的數據,很可能大於前面的最大包(800字節),因此每次傳輸的數據,將會被分割成N個包來傳輸。
因此,用URB來記錄一次完整傳輸的信息,包括每次傳多少,傳幾次,傳的目標位置等。


 

1

2

3


 

psize = my_uvc_wMaxPacketSize; //實時傳輸端點一次能傳輸的最大字節數;lsusb: wMaxPacketSize 0x0320 1x800 bytes;

size = my_uvc_params.dwMaxVideoFrameSize; //一幀數據的最大長度

npackets = DIV_ROUND_UP(size, psize); //傳多少次(向上取整)

  • psize就是每次傳輸的數據大小,通過USB攝像頭的設備描述符wMaxPacketSize(最大每包大小)可以得知。
  • size就是每幀圖像的大小,前面在my_uvc_params已經設置過了,是通過在probe()的打印dwMaxPayloadTransferSize得知的;
  • npackets就是size/psize再向上取整,得到需要傳多少次。
    最後還要size = psize * npackets更新一下向上取整後的新大小。

這個分配MY_UVC_URBS_NUM個(一個就行)urb_bufferurb
urb_buffer通過usb_alloc_coherent()函數分配,大小爲前面的調整後的size,得到指向buf的指針和DMA地址。
urb通過my_uvc_uninit_urbs函數分配,數量爲npackets,得到指向該urb的指針。

對應的,如果分配失敗,相應的調用usb_free_coherent()usb_free_urb()釋放空間,並相應的清空指針和重置my_uvc_q.urb_size

然後就是設置URB:

urb->dev:指向目標設備的指針;<這裏是USB攝像頭my_uvc_udev>
urb->pipe:與目標設置的管道;<這裏使用usb_rcvisocpipe()創建等時(ISO:Isochronous)管道,參數是對應VS的端點地址>
urb->transfer_flags:傳輸標誌;<URB_ISO_ASAP(開始調度)和URB_NO_TRANSFER_DMA_MAP(使用DMA對應的buf)>
urb->interval:傳輸間隔;<來自USB描述符的bInterval=1>
urb->transfer_buffer:要傳輸的buf;<前面得到的my_uvc_q.urb_buffer[i]指針>
urb->transfer_dma:buf對應的dma物理地址;<前面得到的my_uvc_q.urb_dma[i]地址>
urb->complete:收完數據後的中斷處理函數;<後面再編寫>
urb->number_of_packets:該URB要傳輸多少個包;<前面計算的npackets>
urb->transfer_buffer_length:總共的數據長度;<前面計算的size>
urb->iso_frame_desc[j].offset:每個包的偏移位置;<j * psize就對應每個包的偏移>
urb->iso_frame_desc[j].length:每個包的大小;<前面得到的psize>
關於URB數據結構的參考博客。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82


 

static void my_uvc_uninit_urbs(void)

{

unsigned int i;

for (i = 0; i < MY_UVC_URBS_NUM; ++i)

{

//釋放usb_buffers

//同時判斷urb大小,如果非0才執行,因爲本函數最後會將其置0,streamoff調用時,就不應該再釋放一次

if (my_uvc_q.urb_buffer[i] && my_uvc_q.urb_size)

{

usb_free_coherent(my_uvc_udev, my_uvc_q.urb_size, my_uvc_q.urb_buffer[i], my_uvc_q.urb_dma[i]);

my_uvc_q.urb_buffer[i] = NULL;

}

//釋放urb

if (my_uvc_q.urb[i])

{

usb_free_urb(my_uvc_q.urb[i]);

my_uvc_q.urb[i] = NULL;

}

}

my_uvc_q.urb_size = 0;

}

static int my_uvc_alloc_init_urbs(void)

{

int i, j;

int npackets;

unsigned int size;

unsigned short psize;

struct urb *urb;

psize = my_uvc_wMaxPacketSize; //實時傳輸端點一次能傳輸的最大字節數;lsusb: wMaxPacketSize

size = my_uvc_params.dwMaxVideoFrameSize; //一幀數據的最大大小

npackets = DIV_ROUND_UP(size, psize); //傳多少次(向上取整)

if (npackets == 0)

return -ENOMEM;

size = my_uvc_q.urb_size = psize * npackets; //取整後新大小

for (i = 0; i < MY_UVC_URBS_NUM; ++i)

{

/* 1.分配usb_buffers */

my_uvc_q.urb_buffer[i] = usb_alloc_coherent(my_uvc_udev, size,

GFP_KERNEL | __GFP_NOWARN, &my_uvc_q.urb_dma[i]);

/* 2.分配urb */

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

if (!my_uvc_q.urb_buffer[i] || !my_uvc_q.urb[i]) //如果分配失敗

{

my_uvc_uninit_urbs();

return -ENOMEM;

}

}

/* 3. 設置urb */

for (i = 0; i < MY_UVC_URBS_NUM; ++i)

{

urb = my_uvc_q.urb[i];

urb->dev = my_uvc_udev;

urb->pipe = usb_rcvisocpipe(my_uvc_udev, my_uvc_bEndpointAddress); //lsusb: bEndpointAddress 0x82

urb->transfer_flags = URB_ISO_ASAP | URB_NO_TRANSFER_DMA_MAP;

urb->interval = 1; //lsusb: bInterval 1

urb->transfer_buffer = my_uvc_q.urb_buffer[i];

urb->transfer_dma = my_uvc_q.urb_dma[i];

urb->complete = my_uvc_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;

}

現在我們就設置好了URB,包含了目標設備USB攝像頭和urb_buffer等信息,只要把這個URB傳給USB核心,USB核心就會解析URB,與指定的USB設備傳輸數據,數據將被放在urb_buffer裏,接收到USB設備傳來的數據包時,將產生一箇中斷,執行中斷處理函數my_uvc_video_complete
中斷函數裏會依次處理每個包,將包的數據放到my_uvc_q.irqqueue隊列首個節點所指的buf,當多個包的數據量足夠一幀時,就喚醒休眠的應用層,應用層就會得到數據,最後中斷程序再發送URB,再次進入中斷,依次循環。


下面就是實現my_uvc_video_complete,在裏面首先判斷之前URB傳輸的結果:


 

1

2

3

4

5

6

7

8

9

10


 

switch (urb->status) {

case 0: //Success

break;

case -ETIMEDOUT: //Nak

case -ECONNRESET: //Kill

case -ENOENT:

case -ESHUTDOWN:

default: //Error

return;

}


只有urb->status = 0才表示傳輸成功,否則都直接返回。

 

然後在判斷my_uvc_q.irqqueue隊列不爲空的情況下,取出首個buf,後面將從URB得到的數據放在這個buf裏:


 

1

2

3

4


 

if (!list_empty(&my_uvc_q.irqqueue)) //判斷是不是空隊列

buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);//取出首buf用於後續存放數據

else

buf = NULL;

 

之後便是對每個URB的子包進行處理:

1.判斷狀態urb->iso_frame_desc[i].status小於0,跳過處理該子包;
2.計算數據源(來自URB)、長度、目的地址(放到隊列提取的buf);
3.判斷該包數據是否有效,其中data[0]包含頭部長度,data[1]包含錯誤狀態;
4.使用攝像頭廠家提供的特殊處理,完成對fid的操作;<fid介紹見下面>
5.如果buf=NULL,表示之前irqqueue隊列沒有空間了,沒必要後續操作了;
6.判斷buf->state是不是VIDEOBUF_ACTIVE(正在接收數據)狀態,即是不是第一次開始接收數據,是的話改爲VIDEOBUF_ACTIVE
7.讓last_fid = fid,表示要開始接收本幀數據;
8.傳輸的數據長度爲:子包去除頭部信息後的數據長度與buf剩餘空間的 最小值;
9.將URB子包複製到buf中;
10.引用廠家代碼,對buf數據進行某些處理;
11.當子包數據長度大於該buf剩下空間、得到標誌UVC_STREAM_EOF且收到數據不爲空時,表明一幀數據傳完,修改buf狀態VIDEOBUF_DONE
12.從irqqueue隊列刪除該節點;喚醒應用層讀取mainqueue隊列的數據,即本幀數據;修改mem偏移和date_len,取出下一個buf;

以上就是對每個子包的操作,主要包含了子包狀態的判斷對是否完成一幀傳輸的判斷複製子包數據到buf廠家特殊處理再次從隊列獲取buf

講一下fid(frame id)。
我們看到的連續視頻,可以分成若干個1s的視頻,再把每個1s的視頻分成30份,每一份就是一張圖片,稱之爲幀(frame)。
這個幀的數據,是由URB傳輸中的若干個pack組成的,在URB傳輸中,產生一連續的pack,我們如何知道其中的某幾個pack屬於某一幀的呢?
攝像頭廠家的解決方案是,爲每個pack也編號,屬於同一幀的幾個連續pack編號相同,這就實現了在pack上出現0、1交替時,就表示該幀傳輸完了,開始傳輸下一幀。

在中斷函數的最後,還要再次提交URB,這樣才能再次進入中斷,拷貝數據,如此反覆。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155


 

static void my_uvc_video_complete(struct urb *urb)

{

int ret, i;

unsigned char *mem;

unsigned char *src, *dest;

struct my_uvc_buffer *buf;

int len, maxlen, nbytes, data_len;

static int fid, last_fid = -1;

//要修改影像資料,必須先宣告一個特別型態的指標變數,才能正確存取記憶體中的資料

unsigned char *point_mem;

static unsigned char *mem_temp = NULL;

//初始化暫存用的記憶體位置

static unsigned int nArrayTemp_Size = 1000;

printk("enter %s\n", __func__);

printk("=======urb->status: %d ======\n", urb->status);

switch (urb->status) {

case 0: //Success

break;

case -ETIMEDOUT: //Nak

case -ECONNRESET: //Kill

case -ENOENT:

case -ESHUTDOWN:

default: //Error

return;

}

/* 從irqqueue隊列中取出首個緩衝區 */

if (!list_empty(&my_uvc_q.irqqueue)) //判斷是不是空隊列

buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);//取出首buf用於後續存放數據

else

buf = NULL;

for (i = 0; i < urb->number_of_packets; ++i) //一次urb傳輸包含number_of_packets個子包

{

if (urb->iso_frame_desc[i].status < 0)

continue;

src = urb->transfer_buffer + urb->iso_frame_desc[i].offset; //數據源

len = urb->iso_frame_desc[i].actual_length; //數據長度

if(buf)

dest = my_uvc_q.mem + buf->buf.m.offset + buf->buf.bytesused; //目的地址

//判斷數據是否有效;URB數據含義: data[0]->頭部長度;data[1]->錯誤狀態

if ((len < 2) || (src[0] < 2) || (src[0] > len) || (src[1] & UVC_STREAM_ERR))

continue;

if (my_uvc_udev->descriptor.idVendor == 0x1B3B) /* ip2970/ip2977 */

{

if ( len >= 16 ) // have data in buffer

{

// 資料必須從data[12]開始判斷,是因為前面的資料是封包專用

if ( (src[12] == 0xFF && src[13] == 0xD8 && src[14] == 0xFF) ||

(src[12] == 0xD8 && src[13] == 0xFF && src[14] == 0xC4))

{

if(last_fid) //效果:取反

fid &= ~UVC_STREAM_FID;

else

fid |= UVC_STREAM_FID;

}

}

}

else

{

fid = src[1] & UVC_STREAM_FID;

}

/* Store the payload FID bit and return immediately when the buffer is NULL.*/

if (buf == NULL)

{

last_fid = fid;//?必要性?

continue;

}

if (buf->state != VIDEOBUF_ACTIVE) //!= VIDEOBUF_ACTIVE, 表示"之前還未接收數據"

{

if (fid == last_fid)

continue; //因爲是第一次接收數據,前面的fid已經被取反,不該等於上一次的last_fid

buf->state = VIDEOBUF_ACTIVE; //表示開始接收第1個數據

}

last_fid = fid; //開始傳本幀數據

len -= src[0]; //除去頭部後的數據長度

maxlen = buf->buf.length - buf->buf.bytesused; //緩衝區最多還能存多少數據

nbytes = min(len, maxlen);

//dest = my_uvc_q.mem + buf->buf.m.offset + buf->buf.bytesused; //目的地址

memcpy(dest, src + src[0], nbytes); //複製數據

buf->buf.bytesused += nbytes; //更新buf已使用空間

/* ip2970/ip2977 */

if (my_uvc_udev->descriptor.idVendor == 0x1B3B)

{

if(mem_temp == NULL)

{

mem_temp = kmalloc(nArrayTemp_Size, GFP_KERNEL);

}

else if(nArrayTemp_Size <= nbytes) //當收到的資料長度大於上一次的資料長度,則重新分配所需的空間+

{

kfree(mem_temp);

nArrayTemp_Size += 500;

mem_temp = kmalloc(nArrayTemp_Size, GFP_KERNEL);

}

memset(mem_temp, 0x00, nArrayTemp_Size);

// 指向資料儲存的記憶體位置

point_mem = (unsigned char *)dest;

if( *(point_mem) == 0xD8 && *(point_mem + 1) == 0xFF && *(point_mem + 2) == 0xC4)

{

memcpy( mem_temp + 1, point_mem, nbytes);

mem_temp[0] = 0xFF;

memcpy(point_mem, mem_temp, nbytes + 1);

}

}

/* 判斷一幀數據是否已經全部接收到 */

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))

buf->state = VIDEOBUF_DONE;

/* 當接收完一幀數據,從irqqueue中刪除這個緩衝區,喚醒等待數據的進程 */

if ((buf->state == VIDEOBUF_DONE) || (buf->state == VIDEOBUF_ERROR))

{

list_del(&buf->irq);

wake_up(&buf->wait);

mem = my_uvc_q.mem + buf->buf.m.offset;

data_len = buf->buf.bytesused;

/* 取出下一個buf */

if (!list_empty(&my_uvc_q.irqqueue))

buf = list_first_entry(&my_uvc_q.irqqueue, struct my_uvc_buffer, irq);

else

buf = NULL;

}

}

/* 再次提交URB */

if ((ret = usb_submit_urb(urb, GFP_ATOMIC)) < 0)

{

printk("Failed to resubmit video URB (%d).\n", ret);

}

}

3.6 啓動/停止

應用層調用ioctl()傳入的參數爲VIDIOC_STREAMON時,就會調用vidioc_streamon()啓動攝像頭採集數據。
在該驅動函數裏面,主要做三件事:

  1. 設置USB攝像頭參數;(比如使用何種視頻數據格式、何種分辨率)
  2. 分配設置URB;(調用前面的my_uvc_alloc_init_urbs()函數)
  3. 提交URB,等待中斷;

一般的攝像頭,會支持多種格式,比如MJPEG、H264等,也會支持多種分辨率。
因此需要在開始傳輸前,通過USB設置攝像頭,讓其後面返回正確的數據。

假如我們直接設置,可能攝像頭不支持我們設置的格式,後面對應的解析數據可能會出現錯誤。因此我們先嚐試傳入設置參數,攝像頭接收後會保存起來,並根據自身情況做一些修正,再將該設置讀取出來,再進行真正的設置。
這裏我們定義一個my_uvc_streaming_control結構體,用於保存這個設置過程中的參數。

首先是嘗試設置參數,根據攝像頭版本,對應的分配一個data空間,用於等會保存參數進行USB傳輸。


 

1

2

3

4

5


 

//lsusb得到:bcdUVC = 1.00;再BCD轉換,eg:2.10 -> 210H, 1.50 -> 150H

size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根據版本分配buf大小

data = kmalloc(size, GFP_KERNEL);

if (data == NULL)

return -ENOMEM;

 

再清空傳入的my_uvc_streaming_control結構體,設置相應參數,再參考內核UVC驅動使用cpu_to_le16()my_uvc_streaming_control賦值給data


 

1

2

3

4

5

6

7

8


 

memset(ctrl, 0, sizeof * ctrl);

ctrl->bmHint = 1; //保持dwFrameInterval不變

ctrl->bFormatIndex = 1; //支持格式數量

ctrl->bFrameIndex = bFrameIndex; //使用第二種分辨率:640x480(1),320x240(2),160x120(3)

ctrl->dwFrameInterval = 333333; //lsusb: dwFrameInterval(0) 333333 每秒30幀

ctrl_to_data(ctrl, data, size);

 

最後調用usb_control_msg()data傳給攝像頭,這裏的usb_control_msg()在前面的亮度控制詳細介紹了每個參數的含義,當時使用的是VC接口,這裏使用VS接口。
這裏不是真正的設置,所以傳入的參數是VS_PROBE_CONTROL


 

1

2

3

4


 

ret = usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),

SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,

VS_PROBE_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);

kfree(data);

 


嘗試設置了USB後,再把攝像頭修正的參數讀取出來保存到my_uvc_streaming_control結構體中。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27


 

static int my_uvc_get_streaming_params(struct my_uvc_streaming_control *ctrl)

{

int ret = 0;

unsigned char *data;

unsigned short size;

//lsusb得到:bcdUVC=1.00;再BCD轉換,eg:2.10 -> 210H, 1.50 -> 150H

size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根據版本分配buf大小

data = kmalloc(size, GFP_KERNEL);

if (data == NULL)

return -ENOMEM;

//通過usb獲取攝像頭參數

ret = usb_control_msg(my_uvc_udev, usb_rcvctrlpipe(my_uvc_udev, 0),

GET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_IN,

VS_PROBE_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);

if (ret < 0)

goto done;

//返回攝像頭參數

data_to_ctrl(data, ctrl, size);

done:

kfree(data);

return ret;

}

 


最後再將新的參數設置給攝像頭,這樣就能保證現在設置的參數對攝像頭是有效的。
這裏是真正的設置,所以傳入的參數是VS_COMMIT_CONTROL


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22


 

static int my_uvc_set_streaming_params(struct my_uvc_streaming_control *ctrl)

{

int ret = 0;

unsigned char *data;

unsigned short size;

//lsusb得到:bcdUVC=1.00;再BCD轉換,eg:2.10 -> 210H, 1.50 -> 150H

size = my_uvc_bcdUVC >= 0x0110 ? 34 : 26; //根據版本分配buf大小

data = kmalloc(size, GFP_KERNEL);

if (data == NULL)

return -ENOMEM;

ctrl_to_data(ctrl, data, size);

//通過usb嘗試設置攝像頭參數

ret = usb_control_msg(my_uvc_udev, usb_sndctrlpipe(my_uvc_udev, 0),

SET_CUR, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT,

VS_COMMIT_CONTROL << 8, 0 << 8 | my_uvc_streaming_intf, data, size, 5000);

kfree(data);

return ret;

}

 


然後還要指定bAlternateSettingbAlternateSetting用於在同一個接口中的多個描述符中進行切換。
也就是說,USB攝像頭提供多種Interface Descriptor(接口),每個接口的支持一種wMaxPacketSize(帶寬,一次傳輸提供的數據量)、dwMaxPayloadTransferSize(每幀最大數據,實測等於分辨率加512)。
當攝像頭分辨率變化時,相應所需的接口也會變化,比如分辨率變大,要選擇帶寬更大的接口。
bAlternateSetting就相當於是接口的索引,因此不同分辨率,應該選擇對應的接口。比如本次使用的分辨率爲640x480,從my_uvc_params獲取的推薦接口就是bAlternateSetting=6


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20


 

Interface Descriptor:

bLength 9

bDescriptorType 4

bInterfaceNumber 1

bAlternateSetting 6

bNumEndpoints 1

bInterfaceClass 14 Video

bInterfaceSubClass 2 Video Streaming

bInterfaceProtocol 0

iInterface 0

Endpoint Descriptor:

bLength 7

bDescriptorType 5

bEndpointAddress 0x82 EP 2 IN

bmAttributes 5

Transfer Type Isochronous

Synch Type Asynchronous

Usage Type Data

wMaxPacketSize 0x03bc 1x 956 bytes

bInterval 1

 


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14


 

/* 1. 向USB攝像頭設置參數:比如使用哪個format, 使用這個format下的哪個frame(分辨率等) */

// 根據結構體my_uvc_streaming_control設置數據包;再調用usb_control_msg發出數據包;

//a.測試參數

my_uvc_try_streaming_params(&my_uvc_params);

//b.取出參數

my_uvc_get_streaming_params(&my_uvc_params);

//c.設置參數

my_uvc_set_streaming_params(&my_uvc_params);

//d.設置VideoStreaming Interface所使用的setting

//從my_uvc_params.dwMaxPayloadTransferSize得知所需帶寬;實測分辨率不一樣,所需的帶寬也不一樣;

//根據wMaxPacketSize得到對應的bAlternateSetting;

usb_set_interface(my_uvc_udev, my_uvc_streaming_intf, my_uvc_bAlternateSetting);

設置好了攝像頭的format(格式)、frame(分辨率)等,就可以分配設置URB,準備和USB攝像頭傳輸數據了。


 

1

2

3

4

5

6

7


 

/* 2.分配設置URB */

ret = my_uvc_alloc_init_urbs();

if (0 != ret)

{

printk("my_uvc_alloc_init_urbs err : ret = %d\n", ret);

return ret;

}

 

分配完成後,就提交給USB核心,等待中斷來臨,讀取攝像頭髮來的數據。


 

1

2

3

4

5

6

7

8

9

10

11


 

/* 3.提交URB以接收數據 */

for (i = 0; i < MY_UVC_URBS_NUM; ++i)

{

if ((ret = usb_submit_urb(my_uvc_q.urb[i], GFP_KERNEL)) < 0)

{

printk("Failed to submit URB %u (%d).\n", i, ret);

my_uvc_uninit_urbs();

return ret;

}

}

 


停止採集數據vidioc_streamoff()也需要做三件事:

  1. 取消URB傳輸;
  2. 釋放urb_buffer和URB;
  3. 設置接口爲0,讓其處於休眠狀態;

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23


 

static int my_uvc_vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type t)

{

struct urb *urb;

unsigned int i;

printk("enter %s\n", __func__);

/* 1. kill all URB */

for (i = 0; i < MY_UVC_URBS_NUM; ++i)

{

if ((urb = my_uvc_q.urb[i]) == NULL)

continue;

usb_kill_urb(urb);

}

/* 2. free all URB */

my_uvc_uninit_urbs();

/* 3. 設置VideoStreaming Interface爲setting 0 */

usb_set_interface(my_uvc_udev, my_uvc_streaming_intf, 0);

return 0;

}

3.7 其它操作函數(mmap和poll)

現在還遺留兩個操作函數mmap()poll(),因爲涉及buf和隊列,前面無法理解,現在應該好理解了。
首先是mmap(),前面提到應用層調用vidioc_queryctrl()時,會讓驅動程序分配若干個buf,也就是my_uvc_q.buf[N]
現在我們需要做的就是把buf映射到用戶空間,以後用戶空間操作映射後的空間,就間接的操作了內核的my_uvc_q.buf[N]

根據傳入的vma->vm_pgoff偏移,對應找到my_uvc_q.buf,如果沒找到或者大小不對,就退出。
如果找到了對應偏移的my_uvc_q.buf,就可以根據該buf的起始地址和偏移得到物理地址addr
再將物理地址傳入vmalloc_to_page()函數得到page結構體,再使用vm_insert_page()函數將page結構體和傳入的vma虛擬地址綁定,以PAGE_SIZE大小分割總的size
最後在使用計數加1,後面vidioc_querybuf()查詢緩存狀態時,用於更新標誌。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48


 

//把緩存映射到APP的空間,以後APP就可以直接操作這塊緩存

static int my_uvc_mmap(struct file *file, struct vm_area_struct *vma)

{

int i, ret = 0;

struct page *page;

struct my_uvc_buffer *buffer;

unsigned long addr, start, size;

printk("enter %s\n", __func__);

start = vma->vm_start;

size = vma->vm_end - vma->vm_start;

//應用程序調用mmap函數時,會傳入offset參數,再根據offset找出指定的緩衝區

for (i = 0; i < my_uvc_q.count; ++i)

{

buffer = &my_uvc_q.buffer[i];

if ((buffer->buf.m.offset >> PAGE_SHIFT) == vma->vm_pgoff)

break;

}

//沒找到對應的my_uvc_q.buffer或大小不對

if ((i == my_uvc_q.count) || (size != my_uvc_q.buf_size))

return -EINVAL;

/* 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)my_uvc_q.mem + buffer->buf.m.offset;

while (size > 0) //循環把size大小的空間變爲page

{

page = vmalloc_to_page((void *)addr);

//把page和APP傳入的虛擬地址掛構

if ((ret = vm_insert_page(vma, start, page)) < 0)

return ret ;

start += PAGE_SIZE;

addr += PAGE_SIZE;

size -= PAGE_SIZE;

}

buffer->vma_use_count++; //引用計數+1

return ret;

}

 


最後是poll()函數,用來確定buf是否準備就緒,即含有數據。
應用層調用poll()時,會嘗試從my_uvc_q.mainqueue隊列取出首個緩衝區,得到其buf->wait,然後調用poll_wait()wait爲標誌,進入休眠。等待中斷裏的wake_up(),再喚醒。根據buf->state返回對應的mask,對應的應用程序就讀取數據。


 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24


 

//APP調用POLL/select來確定緩存是否就緒(有數據)

static unsigned int my_uvc_poll(struct file *file, struct poll_table_struct *wait)

{

struct my_uvc_buffer *buf;

unsigned int mask = 0;

printk("enter %s\n", __func__);

//從mainqueuq中取出第1個緩衝區,判斷它的狀態, 如果未就緒,休眠

if (list_empty(&my_uvc_q.mainqueue))

{

mask |= POLLERR;

goto done;

}

buf = list_first_entry(&my_uvc_q.mainqueue, struct my_uvc_buffer, stream);

poll_wait(file, &buf->wait, wait);

if (buf->state == VIDEOBUF_DONE || buf->state == VIDEOBUF_ERROR)

mask |= POLLIN | POLLRDNORM; //普通或優先級帶數據可讀 | 普通數據可讀

done:

return mask;

}

 

3.8 測試/效果

如前面測試內核自帶驅動一樣,先編譯自己的驅動,然後加載內核自帶的uvcvideo及依賴,然後移除內核自帶的驅動,安裝自己寫的新驅動,運行xawtv應用程序:


 

1

2

3

4

5

6


 

make

sudo modprobe uvcvideo

sudo rmmod uvcvideo

sudo insmod my_uvc.ko

xawtv -noalsa

 

  • 效果:

完整代碼見GitHub

4.總體分析

整體框圖如下:

幾個基本概念:
1.應用層有五個操作函數,其中ioctl下至少有11個基本的操作函數;
2.USB攝像頭有且只有一個VC接口用於控制,可有多個VS接口用於數據傳輸;
3.11個操作函數可以分爲四類:數據buf的操作、攝像頭格式的操作、攝像頭屬性的操作、攝像頭的啓動與停止;
4.數據buf的操作:
  a.根據應用層參數生成指定個數的v4l2_buffer,這些buf又同時在兩個隊列上:mianququeirqquque
  b.攝像頭產生的數據通過VS接口和USB核心的URB,放入irqquque隊列的首buf,並將該buf從該隊列刪除;
  c.應用層取出mianquque隊列的首buf,得到數據,並將該buf從該隊列刪除,此時該buf同時不在兩個隊列上,將被重新放在尾部;
5.攝像頭格式的操作:使用interface_to_usbdev()得到對應接口的USB設備描述符,描述符包含攝像頭的各種特性信息,保存在v4l2_format結構體中;
6.攝像頭屬性的操作:使用·usb_control_msg()通過VC接口設置相關屬性;

有了上面的基本概念,現在開始調用vidioc_streamon()啓動傳輸:
1.設置USB攝像頭對應帶寬接口等;
2.分配usb_buffersurb,設置urb
3.上報urb,USB核心解析urb,向指定接口(攝像頭VS接口)接收數據(放在usb_buffers);
4.urb傳輸完成後產生中斷,中斷裏取出irqquque隊列首buf,將usb_buffers數據放入,並喚醒休眠的poll
5.poll喚醒,vidioc_dqbuf()mianquque隊列取出首buf,返回給應用層,完成了攝像頭數據到應用層的傳輸。

參考文章:
韋東山第三期項目視頻_攝像頭

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章