以往我們說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
。