以往我們說UVC一般搜索到的內容是板端作為主機,外接USB視頻設備並使用UVC去控制,那么板端也就是從機中的UVC是如何實現的。下面就記錄一個海思SDK中的例子,源碼路徑HISDK/mpp/sample/uvc_app。
文件描述
| 文件 | 說明 |
|---|---|
| application.c | 主函數起始 |
| hiuac.c | 提供hiuac對象,負責音頻控制 |
| hiuvc.c | 提供hiuvc對象,負責視頻控制 |
| camera.c | 提供hicamera攝像頭對象,負責hiuvc,hiuac對象控制 |
| uvc_gadget.c | 實現uvc設備操作功能 |
| frame_cache.c | 實現uvc緩存操作功能 |
| histream.c | 實現視頻流操作功能 |
| sample*.c | 實現對接mpp媒體開發框架操作功能 |
對象操作
以文件划分功能,文件的操作函數都為靜態文件作用域(對外部不可見),這些函數最終被賦值到變量中,而這個變量也是靜態的,只能被唯一一個的全局作用域函數get_xx()獲取。
static int __init(void){};
static int __run(void){};
static hicamera __hi_camera =
{
.init = __init,
.run = __run,
};
hicamera *get_hicamera(void)
{
return &__hi_camera;
}
對象分析
直接進入正題以hiuvc對象為切入點,它由初始化、打開、關閉和運行四個部分組成。對象主要負責流程控制,不包含具體實現,其中__init並沒有做任何事,__open和__close為直接調用,__run創建了線程uvc_send_data_thread去循環run_uvc_data,然后主線程就進入循環run_uvc_device狀態。通過查找可以發現這些對象支持函數都指向文件uvc_gadget.c。

__open
深入open_uvc_device函數,最后可以看出它的最終執行的是v4l2常規流程,首先open設備視頻設備節點獲得fd,其次ioctl VIDIOC_UERYCAP去查詢v4l2能力,最后再ioctl VIDIOC_SUBSCRBE_EVENT去設定訂閱事件,如:VC處理(UVC_EVENT_SETUP),VS處理(UVC_EVENT_DATA),開啟流(UVC_EVENT_STREAMON), 停止流(UVC_EVENT_STREAMOFF)。

__close
關閉是打開的相反操作,主要是去close掉打開的描述符,在這之前需要關閉視頻能力。

__run
這個函數會創建線程循環run_uvc_data,自身進入run_uvc_device循環。

run_uvc_data
這個功能塊就負責一件事,在流啟動后去監聽視頻設備描述符,就緒時就通過uvc_video_process_userptr把一幀數據推入UVC視頻緩沖區,具體功能實現先跳過。

run_uvc_device
這個功能塊負責執行UVC事件處理,函數通過select監聽描述符,當描述符就緒時就從視頻設備的事件隊列中出隊一個事件並做處理。當初始化事件完成后會觸發UVC_EVENT_STREAMON事件,對應的執行enable_uvc_video()去啟動流。當不需要據流時觸發UVC_EVENT_STREAMOFF事件去執行disable_uvc_video()停止流。

至此整個框架基本完成,首先open_uvc_device打開視頻設備驅動,其次run_uvc_device去控制應用層的視頻流開啟關閉,最后通過run_uvc_data推入流到驅動向外輸出。接下來就看看數據是如何被啟動關閉的又是如何流轉的,這里就需要提到uvc_cache視頻設備緩存管理。
數據緩存
uvc_cache它由有6個幀節點和2個幀隊列組成。其中幀隊列free_queue表示空閑節點隊列,初始化時得到了所有節點的。幀隊列ok_queue表示完成節點隊列,當節點填充完數據后才會被put到這個隊列當中。
create_uvc_cache
create_cache_node_list
node = malloc //創建6塊
put_node_to_queue(uvc_cache->free_queue, node)
數據制造
前面提到,當觸發UVC_EVENT_STREAMON事件是會執行enable_uvc_video去啟動流,可用看到啟動通常是先經過清理關機再開機的方式。直接進入histream_startup()這個函數動作,以看到最終創建了一條線程不斷去監聽Venc描述符,當圖像就緒時去獲取保存。首先會從空閑隊列free_queue中取出節點,然后填充幀數據,最后put到完成隊列ok_queue中,這是節點在隊列中的第一次位置交換。函數最后可用看到dev-streaming被置1這就標志着流被開啟,上面說到的run_uvc_data根據這個狀態就可以開始推數據了。

數據首次消費
到這里數據流是開啟了,但是初始化並沒有完成,對於視頻設備/dev/video目前也就僅經歷了open和訂閱UVC_EVENT_操作。接下來enable_uvc_video會ioctl VIDIOC_REQBUFS去命令驅動申請緩存空間。接着從完成隊列ok_queue中取出節點,並將節點成員node->mem賦值到v4l2_buf由ioctrl VIDIOC_QBUF入隊到內核緩存空間中,這個node也還被記錄在等待隊列__waited_node[]上表示這個節點真正被處理。到這里就完成了視頻幀的第一次消費。

數據后續消費
初始化后,數據制造者_SAMPLE_COMM_VENC_SaveData()會不斷的從free_queue取出節點填充並掛到ok_queue上。而后續的消費工作也交回到線程的run_uvc_data去處理。可以看到首先會ioctl VIDIOC_DQBUF從內核出隊一個幀,並從完成隊列取出一個節點,被出隊的幀號也對應等待隊列的標號,幀可以出隊就表示它被處理完了,這時對應等待隊列中的節點__waited_node[buf->index] 就可以把他放回空閑隊列中去,並記錄下本次取出節點。同樣節點成員node->mem也將通過v4l2_buf被ioctrl到內核中。這各過程被重復執行就實現了內核與用戶數據源源不斷的輪換。

結束工作
當UVC_EVENT_STREAMOFF事件到來時,表示結束當前工作,首先ioctl VIDIOC_STREAMOFF停止內核流傳輸,接着關閉應用成流生產, 最后清空完成隊列ok_queue。

