非常感谢下面两位高人

作者: Douglas Boling

译:   MoonLord

 

WinCE下被询问次数最多的驱动是USB摄像头驱动,其原由并不难理解。首先,没个人都喜欢看视频。插上摄像头并用它来捕获视频或静态图像,然后在本地欣赏或者将其发布到网络上,这是一件非常酷的事情。其次,有大量Wince下的驱动程序被公开,所以诚实的说,还是有很多种类的驱动有待开发。最后,虽然有WinCE有现成的1394端口摄像头驱动可以获得,但是更多的系统对USB的支持要多于对1394端口的支持。考虑到这些因素是写一个USB摄像头驱动的时候了。

工作的目标确立后,接下来就要确定一个范围。哪些摄像头需要被支持?哪些特性需要被支持?驱动需要暴露哪些接口给系统?所有这些问题都将影响完成驱动开发所需的时间和精力。

幸运的是,对于选择则哪些摄像头的问题还不难解决的因为USB组织发布了USB视频规范。按照规范编写的驱动程序可以不依赖于任何一款指定的摄像头,同时可以不必暴露任何私有接口给摄像头。不幸的是,即便有遵从此规范的网络摄像产品,也是非常新和非常少的。为了解决这个问题我选择了逻辑摄像头,这个品牌的摄像头市场上非常多。可以确定,逻辑摄像头没有公布适当的interface ID,这说明逻辑摄像头支持视频接口规范。逻辑摄像头确实支持大多数规范。

关于驱动的特性的确定还有一些问题,最初我的目标只是写一个能够捕获静态图像的的驱动。然而当驱动完成时,流接口也被添加了进去。此外驱动还支持各种视频属性,如对比度和明暗度调节。在这里USB视频规范起到了帮助作用,规范列出了一系列可能被摄像头支持的属性,规范还提供了哪些属性可以被特定摄像头支持的发现机制,最后规范还提供了一套相应的查询和设置摄像头属性的命令。

当谈到怎样能够让驱动将摄像头暴露给操作系统的时候,就有一些问题了。暴露流视频给视窗操作系统比较合适的方法是提供一个兼容DirectX的摄像头接口。然而DirectX资源被Windows Mobile5.0支持却不被Windows CE5.0支持。此款驱动还有支持Windows CE4.2的第二目标,所以花费那么多的时间去暴露如此复杂的接口却没有操作系统支持,这样很难大量应用。最后,还是决定让驱动支持简单的流接口,就是用一系列简单的自定义IOCTL 命令来读取驱动程序的静态和动态图像数据。

 

许多人只是想根据该驱动立刻编写应用程序,所以在讲驱动之前让我们先讨论一下编程接口。不想了解驱动程序的读者可以直接跳到文档结尾的测试程序部分。应用程序将把视频直接从摄像头输出到你的WinCE系统桌面上。

The source code for this driver, along with the test program are provided on asdfasdf.

 

Working with the Driver
网络摄像头驱动的编程接口在webcamSDK.h中作了介绍。webcamSDK.h中定义了IOCTL的命令和用来与驱动通信的数据结构。应用程序或其他驱动如果想要与本驱动对话应该调用CreateFile函数。驱动的命名以CAM来命名。但是如果USB总线上有不止一个摄像头设备的时候,就会有很多驱动的实体,系统执行的命令将限制系统只选择其中一个摄像头驱动的实体。

如下代码用来打开摄像头驱动。

// Open Driver

hCam = CreateFile (TEXT("CAM1:"), GENERIC_WRITE | GENERIC_READ,

                   0, NULL, OPEN_EXISTING, 0, NULL);

if (hCam == INVALID_HANDLE_VALUE)

{

    rc = GetLastError();

    printf ("can't open the driver rc = %d\r\n", rc);

}

大多数IO控制命令如ReadFile、WriteFile 和SetFilePointer都不被驱动程序所管理,所以在应用程序结束驱动程序时,必须调用CloseHandle函数来关闭ReadFile返回的句柄。在关闭驱动程序之前,应用程序必须停止任何视频流。因为接口使用的是IOCTL命令,下面我们来看一下这些命令:

IOCTL_CAMERA_DEVICE_QUERYPARAMETERARARY 命令返回一个结构体数组,这个数组介绍了摄像头所支持的特性。视频规范所支持的特性定义如下:

#define FEAT_SCANNING_MODE              1

#define FEAT_AUTO_EXPOSURE_MODE         2

#define FEAT_AUTO_EXPOSURE_PRI          3

#define FEAT_EXPOSURE_TIME_ABS          4

#define FEAT_EXPOSURE_TIME_REL          5

#define FEAT_FOCUS_ABS                  6

#define FEAT_FOCUS_REL                  7

#define FEAT_IRIS_ABS                   8

#define FEAT_IRIS_REL                   9

#define FEAT_ZOOM_ABS                  10

#define FEAT_ZOOM_REL                  11

#define FEAT_PANTILT_ABS               12

#define FEAT_PANTILT_REL               13

#define FEAT_ROLL_ABS                  14

#define FEAT_ROLL_REL                  15

#define FEAT_FOCUS_AUTO                16

#define FEAT_PRIVACY                   17

#define FEAT_BRIGHTNESS                18

#define FEAT_CONTRAST                  19

#define FEAT_HUE                       20

#define FEAT_SATURATION                21

#define FEAT_SHARPNESS                 22

#define FEAT_GAMMA                     23

#define FEAT_WHITE_BAL_TEMP            24

#define FEAT_WHITE_BAL_COMPONENT       25

#define FEAT_BACKLIGHT_COMPENSATION    26

#define FEAT_GAIN                      27

#define FEAT_POWER_LINE_FREQ           28

#define FEAT_AUTO_HUE                  29

#define FEAT_AUTO_WHITE_BAL_TEMP       30

#define FEAT_AUTO_WHITE_BAL_COMPONENT  31

#define FEAT_DIGITAL_MULTIPLIER        32

#define FEAT_DIGITAL_MULTIPLIER_LIMIT  33

#define FEAT_ANALOG_VIDEO_STANDARD     34

#define FEAT_ANALOG_VIDEO_LOCK_STATUS  35

 

驱动程序为每一个所支持的特性返回一个FEATUREPROP结构体,其定义如下:

typedef struct {

    DWORD dwFeatureID;      // Feature ID value (FEAT_xxx value above)

    DWORD dwFlags;      // Flags for the feature

    int nMin;           // Minimum value supported

    int nMax;           // Maximum value supported

} FEATUREPROP, *PFEATUREPROP;

结构体变量dwFlags的值包括:FLAG_FP_ERROR,表明当查询最大值和最小值时发生错误。

                     FLAG_FP_BITFIELD表明特性不是一个数值而是一个bit域。

一旦知道了所支持的参数列表,下一步的任务就是查询当前值和设置参数。命令IOCTL_CAMERA_DEVICE_QUERYPARAMETER

和IOCTL_CAMERA_DEVICE_SETPARAMETER用来完成这个任务。只要应用程序设置了参数,它就应该立刻读回参数的值来看一下哪个摄像头被接受了,因为参数有可能不被摄像头所接受。例如,一个摄像头在当前被设置了自动白平衡,那么改变摄像头的设置可能没有反应。

 

应用程序可以使用IOCTL_CAMERA_DEVICE_QUERYVIDEOFORMATS命令来查询摄像头所支持的视频格式,这条命令返回一个结构体FORMATPROPS 其定义如下:

typedef struct {

    DWORD cbSize;           // Size of the structure

    WORD  wFormatType;      // Video format ID

    WORD  wFormatIndex;     // Video format index

    WORD  wFrameIndex;      // Video frame size index

    DWORD dwWidth;      // Width of frame

    DWORD dwHeight;     // Height of frame

    DWORD dwMaxBuff;        // Maximum size of single frame data

    int   nNumInterval;     // Number of frame intervals supported

                    // If zero, frame interval values are

                    //   not descrete but are continuous. If 0,

//   dwInterval[0] is min value and

//   dwInterval[1] is max value and

//   dwInterval[2] is step value

    DWORD dwInterval[MAXINTERVALS];

} FORMATPROPS, *PFORMATPROPS;

结构体变量wFormatType 用来表示视频数据格式,所支持的格式定义如下所示:

 

#define VIDFORMAT_UNCOMPRESSED      0x0005

#define VIDFORMAT_MJPEG         0x0007

#define VIDFORMAT_MPEG2TS           0x000A

#define VIDFORMAT_DV            0x000C

 

结构体变量wFormatIndex 和 wFrameIndex提供特定格式的标识。当对一个流指定视频格式时,就是使用这两个变量来告诉视频驱动对流使用何种格式和方法。

 

结构体变量wFormatIndex 和 wFrameIndex用来示明水平和垂直的解析度。dwMaxBuff用来表示用来接收一祯数据所需的最大内存。因为数据通常是被压缩的,所以这个值没有必要等于一祯的长乘宽乘像素。结构体变量nNumInterval 和 dwInterval用来表示摄像头每祯的传输速度(用毫秒衡量)。时间间隔可以根据摄像头按照两种方式描述。nNumInterval的值如果是非0则表示有多少不连续的时间间隔在dwInterval数组中被定义。nNumInterval的值如果是0,那么摄像头将接受连续时间间隔。在这个例子下, dwInterval[0]表示最小时间间隔,dwInterval[1]表示最大时间间隔,dwInterval[2]表示两个有效时间间隔的步增。

需要注意的是因为摄像头支持特定的解决方案和时间间隔,但这并不意味着USB总线拥有可获得的带宽或者Windows CE设备有足够的速度去读取大量的数据。

 

一个指定格式视频流的请求要靠IOCTL_CAMERA_DEVICE_STARTVIDEOSTREAM命令来完成,需要传递给该命令的参数是结构体STARTVIDSTRUCT,其定义如下:

typedef struct {

    DWORD cbSize;           // Size of the structure

    DWORD dwFlags;      // STARTVIDFLAG_xxx flags

    WORD  wFormatIndex;     // Video format index

    WORD  wFrameIndex;      // Video frame size index

    DWORD dwInterval;       // Requested video frame interval

    DWORD dwNumBuffs;       // Number of buffers (>= 3)

    DWORD dwPreBuffSize;    // Size of prebuffer

    DWORD dwPostBuffSize;   // Size of post buffer

} STARTVIDSTRUCT, *PSTARTVIDSTRUCT;

 

结构体变量cbSize需要赋值为结构体的大小。wFormatIndex 和 wFrameIndex指定了指定了摄像头的格式和解决方案。这些值直接与IOCTL_CAMERA_DEVICE_QUERYVIDEOFORMATS所返回的解构体FORMATPROPS数组中的值相关。dwInterval要从FORMATPROPS 结构体中设置有效的时间间隔(毫秒级)。

dwNumBuffs表明驱动需要分配多少数据帧的缓存区给视频流。驱动至少需要3个缓存区才能得到很好的效果,这些缓存区都由驱动来分配,而不是调用应用程序或者由驱动和应用程序共同确定所需内存空间。当应用程序需要得到一祯时驱动会将一个有效的缓存地址指针传递个应用程序。在这种方式下,在驱动和应用程序之间传递的只是指像数据的指针,而不是每一帧的数据拷贝。

dwPreBuffSize 和 dwPostBuffSize允许应用程序让驱动在每一帧的帧数据之前或之后分配有效空间。这样可以使得应用程序为每一祯数据加上前缀或头数据以便将祯转换成不同的格式。这个中特性可以用来在数据流传输中将移动帧数据的需要减少的最小。

当使能一个视频流时,驱动程序将在应用程序的请求下设置时间间隔和解决方案。驱动负责和摄像头协商采取一个摄像头能够支持的祯间隔和压缩率。在有些情况下驱动设置的画面祯时间间隔比应用程序实际请求的要低。

要决定当前视频流的时间间隔,应用程序可以通过用IOCTL_CAMERA_DEVICE_GETCURRENVIDEOFORMAT.命令来进行设置。驱动程序将会返回一个数据结构FORMATPROPS(前面介绍过),这个结构包含了当前流的信息。流的时间间隔信息在dwInterval[0]中。

一旦流传输开始,驱动程序可以通过IOCTL_CAMERA_DEVICE_GETNEXTVIDEOFRAME命令请求视频数据的一帧。这条命令实际上有两种用法:第一个是请求视频帧的数据,第二个是返回一个祯缓冲区的指针给驱动程序。应用程序将填写结构体GETFRAMESTRUCT中的数据,然后传给驱动程序。GETFRAMESTRUCT定义如下:

typedef struct {

    DWORD cbSize;           // Size of the structure

    DWORD dwFlags;      // GETFRAMEFLAG_xxx flags

    DWORD dwTimeout;        // Time im mS to wait for frame

    PBYTE pFrameDataRet;    // Ptr to buffer to return to driver

} GETFRAMESTRUCT, *PGETFRAMESTRUCT;

cbSize 赋值为数据结构的大小。dwTimeout填写超时信息,驱动需要等待有效数据祯多长时间。由于实际环境的关系,这个超时时间可能比祯时间间隔要长,比如帧错误和视频流下的静态帧数据隔行扫描。只有在dwFlags 被设置为GETFRAMEFLAG_TIMEOUT_VALID时dwTimeout才会被用到。

如果应用程序正在返回一个缓存指针给驱动程序的缓存池,那么应用程序应该把这个指针赋给pFrameDataRet,并把dwFlags赋值为GETFRAMEFLAG_FREEBUFF_VALID。最终,如果应用程序正在返回一个缓存指针给驱动程序的缓存池但是又不想获得新的帧,那么应用程序可以设置dwFlags为GETFRAMEFLAG_NOFRAME_WANTED。

当发送一个有效的IOCTL_CAMERA_DEVICE_GETNEXTVIDEOFRAME命令后,驱动程序会返回一个GETFRAMESTRUCTOUT结构体,其定义如下:

typedef struct {

    DWORD cbSize;           // Size of the structure

    DWORD dwMissedFrames;   // Number of frames missed

    PBYTE pFrameData;       // Ptr to buffer with new frame data

    DWORD dwFrameSize;      // Size of the data returned

} GETFRAMESTRUCTOUT, *PGETFRAMESTRUCTOUT;

dwMissedFrames表示上次收到获取帧的命令后获得的多少帧。pFrameData用来存储一个指像帧数据的指针。应用程序可以读写数据缓存区,但是在后来的获取帧的命令中它必须返回一个缓存指针给驱动程序,这样缓存区的指针就可以回到驱动的缓存池以便接下来可以写新的帧数据。dwFrameSize用来设置数据帧缓存区的大小。由于摄像头对数据的压缩这个缓存区通常不是实际大小。

当应用程序想停止视频流时,它要使用IOCTL_CAMERA_DEVICE_STOPVIDEOSTREAM命令,此条命令不需要参数。当此条命令发送时,任何没有返回给驱动的缓存区指针都将无效。

为了介绍一系列视频流要用到的命令,下面举一个例子:

DWORD WINAPI ReadFrameThread (PVOID pArg) {

    int rc = 0;

    BOOL f;

    DWORD dwBytes = 0;

    THREADSTRUCT Thd;

 

    // Copy over params

    Thd = *(PTHREADSTRUCT)pArg;

   

    // Initialize the conversion library

    rc = InitDisplayFrame (NULL);

 

    // Parameters needed to start a stream

    STARTVIDSTRUCT svStruct;

    svStruct.cbSize = sizeof (STARTVIDSTRUCT);

    svStruct.wFormatIndex = Thd.wFormat;

    svStruct.wFrameIndex = Thd.wFrame;

    svStruct.dwInterval = Thd.dwInterval;

    svStruct.dwNumBuffs = NUMBUFFS;

    svStruct.dwPreBuffSize = PREBUFFSIZE;

    svStruct.dwPostBuffSize = 0;

 

    // Start the video stream

    f = DeviceIoControl (hCam, IOCTL_CAMERA_DEVICE_STARTVIDEOSTREAM,

                   (LPVOID)&svStruct, sizeof (STARTVIDSTRUCT),

                 0, 0, &dwBytes, NULL);

    if (f) {

        // Call the driver for a frame

        GETFRAMESTRUCT gfsIn;

        GETFRAMESTRUCTOUT gfsOut;

 

        memset (&gfsIn, 0, sizeof (GETFRAMESTRUCT));

        gfsIn.cbSize = sizeof (GETFRAMESTRUCT);

        gfsIn.dwFlags = GETFRAMEFLAG_GET_LATESTFRAME;

       

        memset (&gfsOut, 0, sizeof (GETFRAMESTRUCTOUT));

        gfsOut.cbSize = sizeof (GETFRAMESTRUCTOUT);

 

        // Get a frame of video

        f = DeviceIoControl (hCam, IOCTL_CAMERA_DEVICE_GETNEXTVIDEOFRAME,

                             &gfsIn, sizeof (GETFRAMESTRUCT),  &gfsOut,

                             sizeof(GETFRAMESTRUCTOUT), &dwBytes, NULL);

        fCont = f;

        while (fCont) {

            // Draw frame in HDC

            rc = DisplayFrame (gfsOut.pFrameData, PREBUFFSIZE,

            gfsOut.dwFrameSize, Thd.hdc, &Thd.rect);

 

            // Get the next frame

gfsIn.dwFlags = GETFRAMEFLAG_GET_LATESTFRAME |                  

                GETFRAMEFLAG_FREEBUFF_VALID;

            gfsIn.pFrameDataRet = gfsOut.pFrameData;

 

            // Call the driver

            fCont = DeviceIoControl (hCam,

                IOCTL_CAMERA_DEVICE_GETNEXTVIDEOFRAME,

                &gfsIn, sizeof (GETFRAMESTRUCT), &gfsOut,

                sizeof(GETFRAMESTRUCTOUT), &dwBytes, NULL);

        }

        //

        // Stop the stream

        //

        f = DeviceIoControl (hCam, IOCTL_CAMERA_DEVICE_STOPVIDEOSTREAM,

                     0, 0, 0, 0, &dwBytes, NULL);

    }

    // Clean up translation code

    ReleaseDisplayFrame ();

    return 0;

}

这段代码是WinCE本地机应用程序的一个单独线程,此线程启动时初始化STARTVIDSTRUCT,并向驱动程序发送启动视频的IOCTL命令。在这个例子中,前提是驱动程序已经通过调用CreateFile被打开。一旦视频流被打开,线程向驱动程序发送获得下一帧命令来请求指像数据帧的指针。

为了获得下一帧视频,线程设置GETFRAMEFLAG_FREEBUFF_VALID标志位,然后把之前获得的缓存区指针赋给GETFRAMESTRUCT中的pFrameDataRet,接下来在发送一条获取下一帧的命令。

当fCont被置为false时(无论是由IOCTL命令的失败引起还是由应用程序中的其他代码引起),循环都回终止,线程发送一个停止视频流的命令。此时指像最后一帧的视频数据指针无效而且也不能在被应用程序使用。

驱动程序返回单独一帧的方法很简单,虽然有些慢。应用程序可以通过IOCTL_CAMERA_DEVICE_QUERYSTILLFORMATS命令查询可以支持的捕获到的单独一帧的格式。如果驱动程序正在传送视频流,那么此条命令不但会返回可以支持的视频格式,还会返回当前的视频流格式。如果此时驱动没有进行视频传输,此条命令将会返回所有支持的视频格式。

为了获得静态图像,应用程序要使用IOCTL_CAMERA_DEVICE_GETSTILLIMAGE命令,并且要传递一个VIDFORMATSTRUCT结构体,其定义如下:

 

typedef struct {

    DWORD cbSize;           // Size of the structure

    WORD  wFormatIndex;     // Video format

    WORD  wFrameIndex;      // Video frame size index

} VIDFORMATSTRUCT, *PVIDFORMATSTRUCT;

 

结构体中的变量都是自明的,wFormatIndex 和 wFrameIndex表明获得祯的类型。如果驱动正在传递流视频那么这些结构体变量值将与流格式相匹配,或者命令将会失败。DeviceIoControl功能中的pOut参数必须指向一个足够大的内存空间用来存储静态图像。静态图像的大小将会被写入pdwBytesWritten变量中。

驱动程序提供的IOCTL命令为应用程序提供了完全控制摄像头的能力。共享缓存区的使用避免了驱动与应用程序之间的数据拷贝。这减少了系统的执行命令。由于不支持DirectX这种使用IoContrl命令的接口还算简单。

Windows CE USB客户端驱动
在谈论关于驱动程序本身的执行之前,让我们先来看一下Windows CE USB客户端驱动的架构。Windows CE 下的USB客户端驱动是典型的带有一些额外入口点的流驱动,入口点用来在设备插入时帮助驱动程序安装和加载。

WinCE的流接口与UNIX和DOS下设备驱动的典型流接口很相似。从本质上说,流接口就是由操作系统调用的一系列入口点。其入口点的列表如下:

 

       xxx_Init – Called when the driver is first loaded

       xxx_Deinit – Called when the driver is unloaded

       xxx_Open – Called when the driver is opened by an application or another driver

       xxx_Close – Called when CloseHandle is called by application or driver

       xxx_Read – Called when ReadFile is called by application or driver

       xxx_Seek – Called when SetFilePointer is called by application or driver

       xxx_Write – Called when WriteFile is called by application or driver

       xxx_IOControl – Called when DeviceIoControl is called by application or driver

       xxx_PowerUp – Called when system is entering suspend

       xxx_PowerDown – Called when system is leaving suspend

 

上面所示的入口点的前缀“xxx_”是由三个字母组成的流驱动的名字。例如,一个串口驱动可以用COM为其前缀,所以它的初始化入口点就可以是COM_Init。比较有趣的入口点是xxx_IOControl,它可以让驱动的开发者给驱动程序自定义命令。在这个驱动程序的例子中摄像头属性的读取、动态和静态视频图像的获得都是通过自定义IOCTL命令来完成的。

Windows CE USB客户端驱动所需要的额外入口点如下所示:

 

           USBDeviceAttach – Called when a matching USB device is inserted

 

USBInstallDriver – Called when the driver’s DLL name is entered in the USB Unknown Device dialog box

 

          USBUninstallDriver – Called to have the driver remove its registry information

 

USB入口点提供了两类特定的功能: 安装和设备附属通知。USBInstallDriver 和 USBUninstallDriver被操作系用来添加和删除和USB设备驱动相关联的注册表键值。

USBInstallDriver 和 USBUninstallDriver在辅助驱动安装的同时,USBDeviceAttach功能可以在驱动每次加载的时候调用。实际上,在USBDeviceAttach被调用时,驱动必须告诉操作系统加载DLL为流驱动,这个由ActivateDevice来完成。

对于网络摄像头驱动,USBDeviceAttach通常首先要检测用来描述插入USB设备的USB设备描述符是否和驱动所支持的摄像头相匹配,如果相匹配,驱动则分配和初始化驱动结构体实例。通常接下来调用ActivateDevice加载本驱动为流驱动。最后,驱动注册为USB告知回调程序。作为调用回调函数的结果,驱动程序调用DeactivateDevice来卸载自身。

当摄像头插入USB设备总线后,USB栈会调用USBDeviceAttach,而USBDeviceAttach的调用又回导致ActivateDevice的调用。ActivateDevice的调用回导致CAM_Init被调用。在本驱动中初始化的过程在USBDeviceAttach中进行,所以在Init function中并没有过多的初始化工作要做。

当应用程序或其他驱动调用CreateFile函数来打开本驱动时,CAM_Open入口点被调用。应用程序调用CloseHandle来关闭CreateFile返回的句柄时,CAM_Close入口点被调用。对于打开驱动的功能,必须确定驱动当前还没有打开驱动,网络摄像头驱动只允许一个应用程序调用Open功能一次。CAM_Close通常停止一切视频流。

网络摄像头驱动不使用CAM_Read, CAM_Write或者CAM_Seek入口点。主要的接口是CAM_IoControl入口点。这里,网络摄像头驱动分列了很多唯一的IOCTL命令用来设置摄像头,获得静态图像和动态视频。

Talking to USB Devices
   网络摄像头驱动需要与插入USB总线的摄像头对话。只是系统怎么能知道插入USB总线的设备是摄像头呢?

   USB规范要求USB设备向Host device(the computer)以数据流的形式报告其设备信息以及设备的接口。这个设备描述符包括:卖主,产品,制造商详细的版本ID(VID),产品版本(PID)。除了设备新信息,设备描述符还包括如何通过一系列“接口描述符”和设备进行对话。

接口描述符包括描述接口类型的ID值或者那些不遵从任何标准USB接口的卖主的制定接口编码。如果接口描述符报告了其中的一总USB接口标准,系统就会为这个接口加载一个普通的驱动。如果这个接口是设备供应商特定的接口,那么驱动程序必须查找并使用设备的vendor and product ID(PID 和 VID)。下面列出了当前被USB标准所支持的标准接口:

 

Interface Class                        Interface ID

Audio                                  1

Communications-Control       2

Human Interface Device        3

Monitor                           4

Physical                           5

Image                                 6

Printer                                 7

Mass storage                       8

Hub                                 9

Communications-Data           10

Smartcard                           11

Video                                  13

 

除了interface ID,接口描述符还列出了和接口相关的“end points”(端点); host与设备进行通信通过端点进行。Host通过pipe(管道)连接到端点。对应于备的每一个接口的每一个端点都有一个描述符用来说明这个端点是输入还是输出端点,以及连接到端点的管道类型。

除了在接口描述符介绍的端点外,所有的USB设备都支持控制管道连接到端点0。这条管道在WinCE文档中有时被视为“vendor pipe”,它通常被用来操纵一些低级的设备功能,例如,电量查询和错误状态查询。

The USB Video Specification
USB视频规范介绍了两种接口,一个是控制摄像头的控制接口,另一个是流接口用来发送和接收来自摄像头的视频信息。

控制接口用来控制摄像头的参数,例如明暗度,对比度以及设置和视频流有关的视频格式,帧大小,帧频率和压缩比等参数。此外,控制接口还可以请求摄像头传递静态数据帧。

视频格式、帧大小和帧频率之间的关系非常重要。USB规范允许摄像头返回的视频数据格式有MJPEG, MPEG-2, DV和午压缩的数据,对于每一中格式摄像头都可以返回各种分辨率的帧。例如,160 *120 、320 *240或者更好。摄像头不必支持所有视频格式,它们可能只支持其中的一种或两种。最后对于每一中帧格式和帧大小,摄像头都可以支持一种帧间时隙(视频图象流中两帧之间的时间间隔)的设置。视频控制接口的接口描述符提供了特定摄像头的关于支的持视频格式、帧大小和帧时隙信息

视频数据通过视频流接口,从摄像头流向主机。视频流接口也许有多种可选的视频描述符以供使用。每一个可选择的流接口都有一个拥有不同带宽的端点。为了适当的利用USB的带宽,网络摄像头视频的驱动需要选择一个拥有能够传输视频信息的带宽的接口,但也不要请求过多没必要的带宽。

除了要考虑带宽,在对比图像质量的前提下,驱动程序还要和摄像头设备协调帧大小、帧频率和压缩率。这个过程称之为Probe / Commit过程,驱动先探查摄像头可以支持的参数然后再通知摄像头设置这些参数。probe / commit过程的操作,就像摄像头参数的设置一样通过控制接口进行。视频数据的传输则通过一个拥有适当带宽的接口进行。

The Camera
   用来开发驱动的摄像头是Logitech QuickCam Pro 5000,此款摄像头提供了供应商制定的接口编码,但实际上,这套接口编码与USB视频规范非常相似。

   因为Quickcam Pro 5000提供了供应商制定接口,所以当这款逻辑摄像头被安装时,驱动程序需要添加普通视频接口的注册表项,并登记摄像头的pid和vid。要支持其他摄像头只要将此款摄像头的vid和pid添加到注册表中即可。这种假设情况的前提是,摄像头支持USB规范但是没有提供视频接口ID。很多近期的逻辑摄像头都属于这种情况。

逻辑摄像头用来测试驱动以MJPEG格式和无压缩格式返回数据。但是按照我们的目标,MJPEG格式的数据更有诱惑力,因为这种压缩格式占用更低的带宽。实际上,驱动不关心自己所使用的视频格式,驱动仅仅报告所支持的视频格式给调用它的应用程序,是应用程序请求要使用哪种设备所支持的视频格式。

另一个有趣的方面是,当接入USB1.0和USB2.0时,逻辑摄像头会报告不同的摄像头属性。USB2.0更高的带宽允许摄像头传递无压缩数据,这种特性在USB1.0下不被支持。

读取数据并将数据转换成可以使用的格式是应用程序的职责,在后续的测试程序讨论中,在WinCE系统中将MJPEG帧转换成bitmap格式将会做详细介绍。

The Code
   驱动程序的源文件分布在几个文件中,是按照我们所知的层次驱动结构分布的。在层次驱动中,和操作系统相关的驱动——模块设备驱动(MDD)在一个文件夹下,和硬件相关的驱动——物理设备驱动(PDD)在另一个文件夹下。

   MDD部分的代码包含了流接口的入口点,和WinCE USB驱动所需要的USB客户端支持,本驱动的MDD部分包含两个文件webcam.cpp 和usbcode.cpp。流接口在webcam.cpp中实现,USB入口点在usbcode.cpp中实现。

MDD部分的代码主要分析来自应用程序的的信息并将数据写回给应用程序。实际特定功能的执行在PDD层。数据传输和参数的检查在MDD层的__try __except中,用来抛出因应用程序传来的坏指针引起的异常。这个技术的例子在下面的代码中实现,这段代码使用了IOCTL_CAMERA_DEVICE_GETCURRENVIDEOFORMAT控制命令。

 

//-----------------------------------------------------------------------------

// mdd_GetCurrentFormat - Called to process

// IOCTL_CAMERA_DEVICE_GETCURRENVIDEOFORMAT ioctl

//

int mdd_GetCurrentFormat (PDRVCONTEXT pDrv, PBYTE pOut, DWORD dwOut,

                          PDWORD pdwBytesWritten)

{

    int rc;

    FORMATPROPS Props;

    PFORMATPROPS pPropsOut;

    DEBUGMSG (ZONE_FUNC, (DTAG TEXT("mdd_GetCurrentFormat++\r\n")));

 

    if (!pOut || (dwOut < sizeof (FORMATPROPS)) || !pdwBytesWritten)

        return ERROR_INVALID_PARAMETER;

 

    // Check the output structure

    __try

    {  

        *pdwBytesWritten = 0;

 

        pPropsOut = (PFORMATPROPS)pOut;

 

        if (pPropsOut->cbSize != sizeof (FORMATPROPS))

        {

            DEBUGMSG (ZONE_ERROR, (TEXT("Bad structure size\r\n")));

            rc = ERROR_INVALID_PARAMETER;

        }

    }

    __except (EXCEPTION_EXECUTE_HANDLER)

    {

        DEBUGMSG (ZONE_ERROR,

          (TEXT("Exception writing output data.\r\n")));

        rc = ERROR_INVALID_PARAMETER;

    }

 

    // Call the PDD to do the work

    if (rc == 0)

        rc = pdd_GetCurrentFormat (pDrv, &Props);

 

    if (rc == 0)

    {

        // Write to the output structure

        __try

        {  

            *pPropsOut = Props;

            *pdwBytesWritten = sizeof (FORMATPROPS);

        }

        __except (EXCEPTION_EXECUTE_HANDLER)

        {

            DEBUGMSG (ZONE_ERROR,

          (TEXT("Exception writing output data.\r\n")));

            rc = ERROR_INVALID_PARAMETER;

        }

    }

    DEBUGMSG (ZONE_FUNC, (DTAG TEXT("mdd_GetCurrentFormat-- rc %d\r\n"),rc));

    return rc;

}

 

上面这段代码并不是非常高效的,因为PDD要写信息给一个栈中的结构体,然后MDD把数据拷贝到输出结构体中。但是在这个部分稳定性比速度更重要。我们注意到任何使用输出缓冲区的部分的代码都被放在了__try __except中用来提示坏指针。此外大量的DEBUGMSG宏被用来显示错误原因。

对速度要求很高的部分是传递视频数据流的部分,当传递视频流时,驱动程序采用和上述方式完全不同的方法,在这个部分驱动程序并不将数据从PDD拷贝到MDD然后通知应用程序,驱动程序根本不拷贝任何东西。取而代之的是把视频数据放在共享缓冲区中,这样应用程序就可以随时读取数据。

当视频流开始时,MDD分配足够内存空间用来存放所有的应用程序请求的帧缓冲区。然后,每次PDD将数据流向一个帧缓冲区,接下来流向下一个。当应用程序请求一帧数据时,驱动程序从帧缓冲区列表中找到最新视频帧所在的帧缓冲区,然后将这个缓冲区的指针返回给应用程序。在应用程序进行下一次请求并把指针返回给驱动程序之前驱动程序不能在使用该指针,而应用程序可以一直使用该指针读取数据。

因为内存映射的缓冲区可以被所有应用程序访问,所以应用程序可以访问视频帧缓冲区。这个技术在未来升级的WinCE系统中可能会得到更好的支持,那时,当数据被从摄像头读取时,它将只被写入缓冲区一次,然后直接被应用程序访问。这个技术避免了数据帧从驱动程序到应用程序的拷贝操作。

The Test Program
  本驱动所使用的测试程序是CamTest2,这个测试程序直接使用WinCE提供的Win32 API编写,不依赖于控制代码或MFC库。

  CamTest2依赖一个图像库用来将逻技摄像头的MJPEG格式视频转换成窗口可以显示的bitmap格式。由于这个图像库的依赖,CamTest2并不能在Windows CE 4.2下运行,因为Windows CE 4.2不提供这个图像库。虽然没有测试,但是这个驱动程序没有理由不能在WinCE4.2下运行,要想在Windows CE 4.2下测试驱动需要找一个Windows CE 4.2所支持的图像转换程序。

CamTest2的代码被写在几个源文件中,在CamTest2.cpp, CamSettingsDlg.cpp 和 StillCapDlg.cpp中的代码提供Windows基础应用,看上去和感觉上就像是对话框的代码。在CameraCode.cpp中的代码负责完成测试程序和webcamera驱动之间的交互。这部分代码可放在独立的DLL文件中并提供简单接口给驱动程序。最后,Mjpeg2Bmp.cpp中的代码使用成像库将每一个MJPEG帧转换成bitmap格式。完成转换的这部分代码实际上有一个数据帧拷贝的过程,这个过程完全可以通过COM server的执行被避免掉,这个任务就交给读者来完成了。

正如所料, MJPEG, 或 Motion JPEG与JPEG格式的关系很紧密。MJPEG是一种很方便的格式,因为WinCE5.0有一个成像库可以把MJPEG图像转换成bitmap图像。接下来的任务是将MJPEG格式转换成JPEG格式,这个转换可以通过将帧数据的MJPEG帧头去掉,然后添加JPEG的帧头和适当的颜色帧头来完成。因为颜色帧头在MJPEG,格式总已经预先确定了,所以转换过程非常简单。驱动程序本身并不关系图像格式,而是测试程序将MJPEG格式转换成bitmap格式然后通过BitBlt函数将bitmap图像显示在Windows设备上。

 

下图是测试程序CamTest2在WinCE上运行的效果图;


 

这张图片显示了我桌面上的一些凌乱的东西,帧大小是640*360,频率是10帧每秒,这种捕获率在中等速度的X86系统中是很典型的。速度更慢一些的系统不支持这么大的图片和捕获率,在基于CEPC的系统中可以支持更大的图片和更高的捕获率。

摄像头驱动的编写在许多方面都具有挑战性,但也是非常具有回报的经历,复杂的和灵活的USB视频规范另驱动的编写很头痛,但是一旦带宽的观念,probe / commit的观念和视频流的执行被掌握,剩下驱动程序的编写就很简单。我们所获得收获是获得了让WinCE在系统消耗很小的情况下显示视频图像,作为一个在出生在电视机时代的人,这确实是一个很不错的经历。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dengxin123/archive/2008/11/25/3373377.aspx

 

posted on 2010-08-16 14:56  jiege  阅读(2604)  评论(0编辑  收藏  举报