天天看點

Windows CE USB攝像頭驅動編寫

非常感謝下面兩位高人

作者: 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

繼續閱讀