contributed by < eecheng87
>
GitHub: eecheng87/vcam: Virtual camera device driver for Linux
linux2020
vcam 的開發和 frame buffer 有著高度關聯,故先來了解 frame buffer 到底是什麼。整理自 Wikipedia 的資料:
事實上,frame buffer 是 RAM 的一部分。
frame buffer 之於其他周邊裝置的關係:
[註1] 運作流程是,cpu 上的 memory mapping 到 buffer 中的 RAM,而這塊 RAM 就是存稍後要顯示在裝置上的圖片。
[註2] 現在的顯示卡內部會有 frame buffer 的電路 (integrated circuit),這個電路可以將存在 buffer 內的 bitmap 轉換成真正要輸出 (frame buffer 可以直接 memory mapping 到 cpu 的 memory 上) 的 video signal。(因為是積體電路,所以在 1975 年代後電腦才普遍使用 buffer frame)
frame buffer 可達到的功能:
[註1] 早期電腦不會管你寫什麼東西進 buffer,這樣可以會引起安全上的問題。如:打出的顏色把螢幕燒掉。
控制模式:透過 UNIX 機器和作業系統可以輕易的對硬體操作(ioctl),控制不同的解析度、螢幕更新頻率和顏色深度等等。
[註2] linux frame buffer
以下是兩個版本的 v4l2 的框架圖,能加速對後續的理解。
圖中間的實做在 device.c
v4l2_device 是整個輸入設備的總結構體,可以認為它是整個 v4l2 框架的入口。再往下分是輸入子設備,舉例: MIPI 等設備,它們是屬於一個 V4L2 device 之下。子設備可以有很多個,透過 list 串起來,示意圖如下:
Video4linux2 驅動主要負責從 sensor (通常是通過DMA)上獲取視頻資料然後把這些視頻幀傳輸到使用者空間,大量資料的傳輸有效能議題要考慮。出於此目的,V4L2 定義了複雜的 API 去處理串流資料。V4L2 子系統為實現這些 API 也加入了不少複雜的程式碼,當然大部分的程式碼沿用了 V4L。為了使驅動工程師更方便實現視頻驅動程式碼,videobuf 子系統提供了一組用於管理串流 IO buffer 的介面 (將於稍後詳細解釋 videobuf)。
簡單來說 v4l2 主要是調用一系列的 ioctl
函數去對 v4l2 設備進行打開、關閉、查詢和設置等操作。v4l2 是一個 character driver,其驅動的主要工作就是實現各種各樣的 ioctl。
從模組初始檔案開始追蹤,在 vcam_init
內的 create_control_device
功能如其名,實際做了:
分配 control_device *
空間,包含其成員 vcam_devices
,並歸零部份成員。
建立裝置資料夾
class_create 和 class_device_create 用法
效果:當 module 被載入時,udev 會自動在 /dev
下建立裝置檔案 (建立字元裝置)。所以可以透過 ls | grep clt
發現 /dev
底下的確有 vcamctl
。
[註] 詳細參數用法
登記 major number,可分動態登記和靜態登記(後者過時,不建議。本專案使用前者)。
加載步驟:
1、以 alloc_chrdev_region
動態取得 major number。
2、以 cdev_init
登記系統呼叫 handler。
3、以 cdev_add
向 kernel 登記驅動程式。
卸載步驟:
1、以 cdev_del
向 kernel 釋放驅動程式。
2、以 unregister_chrdev_region
釋放 major number。
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
是依驅動程式名稱用來取得 major number,並從指定的起點開始預留指定數目的 minor number。
cdev_init(struct cdev *cdev, const struct file_operations *fops)
負責初始化 cdev 結構,並登記系統呼叫 handler(fops),另外,其 cdev
結果變數在卸除驅動程式時還要用到,需定義為全域變數。
在呼叫 cdev_init()
後,還要將 cdev
結構變數的 owner
成員設為 「THIS_MODULE」。
cdev_add(struct cdev *p, dev_t dev, unsigned count)
向 kernel 登記 cdev_init() 設定好的裝置資訊。
[註] Major number: 驅動程式載入時,可以向 kernel 登記 major number,每個驅動程式都有獨一無二的 major number。
執行 request_vcam_device
,實際上是呼叫 create_vcam_device
。
v4l2_device_register
是為了註冊 V4L2 裝置(可以在 /dev
底下找到 video0)。 vcam_out_videobuf2_setup
。詳細實作參考 videobuf.c 說明。init_framebuffer
是用來初始化 frame buffer,會在 /proc
底下建立對應的檔案(如:vcamfb
)。所以可以直接對該檔案做寫的動作,接著就會輸出到螢幕。vcam
(即 video_device *vdev
)。通過 video_register_device
,其中選 GRABBER
作為參數。
VFL_TYPE_GRABBER
indicates a frame grabber device - including cameras, tuners, and such.VFL_TYPE_VBI
is for devices which pull information transmitted during the video blanking interval.VFL_TYPE_RADIO
for radio devices.VFL_TYPE_VTX
for videotext devices.vcam_in_queue_setup
在 device.c
中花了很大的篇幅實做 struct v4l2_ioctl_ops vcam_ioctl_ops
成員函數。目的是 videobuf2 子系統要把使用者空間的操作和視頻設備驅動關聯起來。在 網站 中有提到,舉 input 來說,要實做三種版本(.vidioc_enum_input
、.vidioc_g_input
和 .vidioc_s_input
),供使用者選擇。舉例來說,.vidioc_querycap = vcam_querycap
代表當應用程式使用 ioctl
時,且參數是 VIDIOC_QUERYCAP
,則會執行自己實做的函數 vcam_querycap
。
以下簡單說明需要實做的函數
vcam_querycap
:用來填寫裝置的能力。比較重要的是 vcam_querycap
的成員變數 capabilities
,這個變數是紀錄支援的操作。舉例來說,在 vcam 中擁有 V4L2_CAP_VIDEO_CAPTURE
的能力。在應用程式端該如何檢查,應該如下:
vcam_enum_input
… 待補
除了實做 ioctl
的操作,另一個重點是實做 v4l2_file_operations
的成員(函數)。事實上,上個部份的 v4l2_ioctl_ops
和本部份都是操作的集合,之所以要分開是基於設計考量,希望不要那麼複雜,所以分成兩種。(經過觀察,在 v4l2_file_operations
指定的函數都是 /v4l2-core/v4l2-fh.c
提供的,而非自己實做的)。
總結上述提到的兩大類 operation,把他們填回 video_device
的成員變數。
device.c 還有處理 YUV 和 RGB 兩種顏色編碼的轉換。
先來看在檔案的結構 file_operations control_fops
,file_operations 內的成員為函數指標,指向 system call 的實作函數。Linux 驅動程式是透過 file_operations 來建構 VFS 層的支援。而 file_operation 裡的函數指標,即是指向每一個 system call 的實作函數。實做的函數包含:
control_ioctl
指向 unlocked_ioctl
:unlocked_ioctl
取代過時的 ioctl
,想了解詳細原因可參考此。ioctl
的原形是 int ioctl(int fd, unsigned long request, ...)
,和對應的實做 control_ioctl
原形一樣(這是必然的,否則無法透過指標函式的方式指給對方)。control_ioctl
的內容透過 switch 和對應的 command,來決定呼叫什麼指令。所以在 user space 的呼叫大概像
這樣就會執行 case VCAM_IOCTL_CREATE_DEVICE
,即做 request_vcam_device
。
VCAM_IOCTL_CREATE_DEVICE
:會執行 request_vcam_device
,在 /dev
底下建立 videox。VCAM_IOCTL_DESTROY_DEVICE
:刪除裝置。VCAM_IOCTL_GET_DEVICE
:可以得到裝置的資訊,資訊內容即 struct vcam_device_spec
內的成員。VCAM_IOCTL_MODIFY_SETTING
:根據傳入的參數 dev_spec
來填寫規格到 vcam_device
中的 input_format
。control_open
和 control_release
沒有特別實做額外功能。[註]目前觀察起來,專案下種共有 3 種不同的 open
方式,分別是 .open = vcamfb_open
、.open = control_open
和 .open = v4l2_fh_open
。而 vcam-util.c 使用的 open 是 control_open
。
control_read
和 control_write
待補。
最後一個部份是 create_control_device
,這部份的內容已在初始化模組提及。
本檔案和 frame buffer 有極大關聯。最粗淺的觀察可以發現,本檔案會在 /proc
底下建立檔案,此檔案可供寫入,而這塊記憶體被映射到 video。也就是說,可以直接把想顯示在畫面的影像寫入本檔案。這樣的行為的確很像 frame buffer。
接著來仔細研究一下這個檔案。首先,proc 是一個虛擬的檔案系統,我們利用它實現 Linux 核心空間與使用者空間的通訊。在 proc 檔案系統中,我們可以將對虛擬檔案的讀寫作為與核心中實體進行通訊的一種手段,但是與普通檔案不同的是,這些虛擬檔案的內容都是動態建立的。
在 fb.c 中會初始化 frame buffer,即執行 init_framebuffer
。當中呼叫 proc_create_data
,會在 /proc
底下建立 vcamfbx
、設定檔案權限、註冊 file operation 和傳入資料( 此資料即 vcam_device
,可透過 PDE_DATA
取得 )。
以下是 proc_create_data
的原型:
接著來看 file operation 實作的函數。
透過 PDE_DATA
取得在 proc_create_data
傳入的參數(void *data
),然後存在 private_data
。其實 private_data
是用來保存自定義設備結構體的地址的。自定義結構體的地址被保存在 private_data
後,可以在 read,write 等驅動函數中被傳遞和調用自定義設備結構體中的成員。
這邊比較關鍵的是 copy_from_user
的使用,copy_from_user
的原型是:
函數顧名思義,就是要把 user space 的資料搬到 kernel space。而第一個參數是 destination address,即核心的位址;第二個參數就是 source address,即使用者的位址。
從 source code 的調用 copy_from_user(data + buf->filled, (void *) buffer, to_be_copyied)
來觀察,想要將 buffer
複製到 data + buf->filled
,這意味著 data + buf->filled
在 kernel space。
接著推測要怎麼使用這些 API 和呼叫的情境。vcamfb_write
是實作系統呼叫 write
,所以我們預期在 user space 的某處會開啟 /proc/vcambfx
取得 fd
,然後我們對這個 fd
做 write
時,會做 vcamfb_write
。由 write
的原型 write(int fd, const void *buf, size_t count)
可以發現傳入的 buf
是想要寫的東西,對應到 vcamfb_write
的參數是 buffer
。
所以我們可以推測使用的情境應該如下:
在 user space 或 user application 有人會想寫入 frame buffer,開啟 /proc
底下的檔案取得 file descriptor 對其 write
,而其第二個參數 buf
是想寫的字串(此字串是 user space 的位址)。接著呼叫 vcamfb_write
,在此函數中從 buffer
是剛剛傳進來的 buf
。此時,我們要把這個「想寫」入 frame buffer 的字串真正地寫進,所以透過 copy_from_user
達成。將在 user space 的字串 buffer
寫進 kernel space 的 data + offset
。
這部分的程式碼內容和 videobuf2 有極大關連,所以先來研究一下 videobuf2 到底是甚麼。videobuf 已經被 videobuf2 取代,相比 videobuf,videobuf2 更加完善,更加實用。雖然 videobuf 工作的很好,但是在一些方面並不是那麼完善,各種不同的 API 相當依賴 buffer 的類型,也沒有提供相關的介面用於視訊緩衝區管理。Videobuf2 提供了一組統一的 API 介面,允許驅動自己有更多的配置。
若從前面的 v4l2 架構圖可以看出,video buffer 是連接 user space 和 v4l2 driver 的橋樑。 在 videobuf.c 中的 vb2_ops vcam_vb2_ops
實作了一些 call back function 用來完善對緩衝區的操作建構 videobuf2 底層的基礎設施。實作了包含:
queue_setup
(vcam_out_queue_setup
): 在 user space 執行 ioctl:VIDIOC_REQBUFS
操作時被呼叫,作用是用來建立 buffer queue。參數部分,nbuffers
是申請的緩衝數,nplanes
是一個 frame 中所需要 video plane 的數目,sizes
是每個 video plane 的大小(bytes),alloc_ctxs
包含了每個 plane 的 allocation context,這個只會在 contiguous DMA 模式使用到。函數內透過 vb2_get_drv_priv
取得 queue 上的 driver private data,並取出規格利用。
start_streaming
(vcam_start_streaming
): 告訴 driver 甚麼時候開始獲取影片資料。用來回應 ioctl:VIDIOC_STREAMON
操作開始抓取視頻資料,如果驅動程式實現了 read()
也會呼叫到start_streaming()
。實作的內容是開了一條 thread 做 submitter_thread
( device.c 內)。在 submitter_thread
當中,做 submit_copy_buffer
屬最重要,將 rgb (使用者寫入的編碼)編碼轉成 yuvv。
stop_streaming
(vcam_stop_streaming
): 告訴 driver 甚麼時候停止獲取影片資料。用來回應 ioctl:VIDIOC_STREAMOFF
操作停止抓取視頻資料,等待 DMA 停止後函數才會返回。這裡值得注意的是,呼叫 stop_streaming()
後,videobuf2 子系統會回收掉所有傳遞到驅動程式的 buffers,此時驅動程式不能訪問這些 buffers。
wait_prepare
(vcam_outbuf_unlock
): 根據文件內說明實作,簡單來說就是釋放鎖。
release any locks taken while calling vb2 functions; it is called before an ioctl needs to wait for a new buffer to arrive; required to avoid a deadlock in blocking access type.
wait_finish
(vcam_outbuf_lock
): 和 wait_prepare
對應。reacquire all locks released in the previous callback; required to continue operation after sleeping while waiting for a new buffer to arrive.
buf_prepare
(vcam_out_buffer_prepare
)buf_queue
(vcam_out_buffer_queue
)完成類似 file operation 的實作之後,填入結構 vb2_ops vcam_vb2_ops
。接著註冊這些實作好的東西是在 vcam_out_videobuf2_setup
裡面達成。拿到 vb2_queue
開始填充成員,當然包含告知剛剛實作的 ops
(q->ops = &vcam_vb2_ops
)。另外 io_modes
表示該用何種方式來訪問 buffer,vcam 是用 VB2_MMAP | VB2_USERPTR | VB2_READ
表示:
mmap
映射緩衝區到用戶空間。通常 vmalloc
和 contiguous DMA buffers 來分配此方式的緩衝區。read()
方式訪問視頻設備。最後透過 vb2_queue_init
完成初始化。
檢查是否能跑 initial code,檢查步驟可參考工水鳥整理的筆記。
本段落以後均不仔細解釋 driver 實做原理,詳細可參考第二部份 vcam 運作原理內容
新建一個 user application 即跑在 user mode 的程式。目的是為了熟悉和確認對 driver 間的關係是否正確,所以做了一些基本操作。首先在 main 打開在 /dev
底下的 video。這是我們載入 driver 時建立的。
拿到 file descriptor 之後,就可以對其操作。可操作的東西定義在 v4l2_ioctl_ops
和 v4l2_file_operations
中。接著應證一下是否真為所述,以下嘗試使用 ioctl
中 query capability 的功能:
接著可以在 device.c 的 vcam_querycap
中添加 printk
訊息,接著執行 app.c
可以發現 dmesg 中有剛剛添加的訊息,這代表我們在 user application 中的確呼叫了我們實做的 ioctl 操作。
此外,為了開發方便,可以在 Makefile 中加上
這樣能讓 driver 中的 pr_debug
顯現在 dmesg 中。
[註]在追蹤程式碼時可透過 grep --include=\*.{c,h} -rnw -e "regular expression"
來找想找的關鍵字。
This Linux module implements a simplified virtual V4L2 compatible camera device driver with raw framebuffer input.
沒錯!起初我還對 v4l2 不太熟時忽略了文件的第一行,原來 vcam 是一個提供介面讓使用者輸入 raw data 進入了裝置。
在 vcam 中可以選擇兩種編碼,分別輸入的編碼和輸出的編碼,兩者可選是 rgb 或 yuyv。從 device.c 中可以發現輸出的編碼會和輸入的編碼一樣
也就是說在這種安排下根本不會進入第 597 行的轉行格式。當然,如果你把 ==
改成 !=
就會進入轉換的程式碼區塊。但是結果都一樣。
承上,可透過 vcam-util
新增不同輸入編碼的 video,若今天想創一個吃 yuyv 編碼的裝置可透過
若餵入 {65, 66, 67} 進 yuyv 裝置會得到綠色。反之, rbg 裝置會產生灰色。關於兩種編碼的轉換可以透過這個網站來看。
使用 v4l2-capture 程式來抓取 video 的資料。若開啟的是 video0 (視訊鏡頭) 的話,產生的 raw data 可以直接開起來變一張圖片。但是如果要開 vcam-util 建立的 videox 的話就比較奇怪了。首先,想要讀 videox 時候(透過 capture.c),不能同時用 vlc 開 videox 來看目前的畫面該長怎樣(會出現 buffer is busy 的錯誤),這個問題聽說要用 double buffering 來解。但是仍有辦法讀出 videox 的 raw data frame,方式是(app
是參考官方範例對其改造)
如此一來,你可在資料夾底下看到一堆生成的 raw data。但是比較尷尬的是這些 raw data 不像前面開 video0 產生的 raw data 能開起來。我猜可能是因為我們在紀錄 raw data 的格式不對,導致根本開不起來(關於這點在稍後會提出解法)。
在 device.c 中有分 in-buffer 和 out-buffer,分別是編碼轉換的前後。而透過 capture.c 拿出來的資料是 out-buffer 也就是轉換後的結果。可以透過
接在可以在產生的 raw data 拿到 ABCABC…
反之,如果寫到 yuyv 編碼的,你會拿到 ASCII {0, 150, 0} 的資料。
[註]由於 capture 會產生大量 raw date,為了方便,可以執行 find capture_frame_data/ -regex '.*.raw' -delete
即一次刪除那些 raw data。
倘若我們是透過官方網站提供的範例(capture.c),我們讀取出來的資料,即輸入的 raw input(e.g. ABCABC…)。產生這種格式的檔案其實一點幫助都沒有,因為無法開啟觀看。在專案下 cpature_frame_data 底下的 capture.c 中寫檔(捕捉裝置的 buffer 的 pixel 值)的程式片段是:
以上是直接把 buffers[0].start
的 data 直接寫入檔案,而非正確的影像格式(如: jpg)。為了解決這個問題,可以增加將 raw data 轉成 jpg 格式的函數 jpegWrite
:
這樣就可以在同個資料夾下看到『可觀測』之照片。
jpeg 的轉換可以參考第 61 行開始的 static void jpegWrite(unsigned char* img, char* jpegFilename)
,這邊需要注意的是因為有使用 jpeglib.h
函式庫,所以在 Makefile 中必須加引數,否則會編譯失敗:
預期效果是能將特定的圖片,如將專案底下 sample_image
內的圖片顯示在 video1 上(透過 vlc)。
首先找一張圖片,透過 python 提供的套件把圖片大小改成專案統一的規格(),接著透過 PIL
套件將 pixel 值存成文字檔。(可在專底下的 /python 找到相關程式碼 )
執行程式後預期會得到 ../sample_image/1.raw
的 raw data,格式大概如下:
接著在寫一支 C 程式,將 raw data 讀出來再餵入 vcamfb,這邊唯一需要注意的是 unsigned char 的 specifier 是 %hhu
。
測試的方式是:
預期在 vlc 可以看到:
和範例圖片一樣。
倘若使用者沒有寫東西到 vcamfb 時,最出版本的狀況會看到漸層畫面,如以下示意圖:
本功能希望能夠達到和 akvcam 內提供的功能一樣
Configurable default picture in case no input signal available.
[註]關於 akvcam 的建置和操作可以參考下一個部份的整理
為了達到目的,我們需要先找出在 vcam 中實做此功能的程式碼。在 device.c 中submitter_thread
處理著編碼的轉換(in_buf
-> 編碼轉換 -> out_buf
)。雖然看似毫無關聯,但實際上倘若沒有東西寫入 vcamfb 則會造成 in_buf
沒東西,所以這種情況會在 submitter_thread
中處理。以下是submitter_thread
持續在做的區塊,主要判斷是否有東西寫入 frame buffer、buffer 空了等狀況。
在沒有寫入東西的進 vcamfb 的情況下,大部分會走 B
區塊,偶爾會走 A
(但目前找不太到如何控制進 A 或 B)。而有寫東西的進 vcamfb 則進 C,C 會做 scaling 等事情,但不是目前考量的重點。我們現在真正在意的是在 B 會呼叫 submit_noinput_buffer
,接著往這裡仔細看。
原本的效果是會產生漸層畫面,以下是實做的程式碼:
以上實做看起來還算符合預期,手法是一次寫一排(厚度由stripe_size
決定),而每排顏色一樣,隨著排數增加,pixel value 上升。直到 pixel 達灰階最高值 255,剩餘的排數全部填 0xff
。
既然已經知道原版本程式碼寫在哪裡和怎麼實做,接著著手修改想要的效果。在 linux kernel 中要產生隨機的效果需要使用 get_random_bytes,這個函數的使用其實也是 akvcam 產生雜訊效果的所採用。達成效果只要把前面的程式碼改成:
檢視效果的指令:
輸出結果:
在 line 視訊的時候,可以選擇特殊的濾鏡功能,在人臉上增加一些物件。如:人的頭上增加一對兔子耳朵。
示意圖如下:
本部份希望能夠建立在 vcam 的基礎上達到此功能,此功能的程式碼實做在 /extension 底下。以下是模擬的架構:
為了模擬出上述效果,我們需要先建立一些「環境」。首先,我們要模擬移動的人臉,這裡我採用紅方塊代表人臉。而物件(兔子耳朵)會隨著人臉的移動而跟著移動到適合的位置,所以我們也需要製造出人臉(紅方塊)移動的感覺。
預期可以看到以下畫面,且紅方塊會隨機移動。
…
原專案在傳入 rgb 格式的編碼,會在 kernel module 中做編碼的轉換,轉成 yuyv。因為在 kernel 沒辦法做浮點數運算,所以 vcam 用了一些方法避開此點。本功能是希望能夠在 user space 做提前的轉換,再在輸入餵入 vcamfb。雖然不知道這樣的效益怎麼樣,硬要講的話,好處大概就是能夠縮減 kernel 大小,和更精準。
在 vcam 中有提到 3 個 v4l2 相關的專案,選擇 akvcam 的原因是因為它較年輕,而且目前仍有在維護,所以問題可能會少一點,接著會簡單介紹如何建置,讓它順利跑起來。並提供簡單的範例。
稍後的實際操作可以透過這張圖更了解 akvcam 到底在幹麻
首先你需要準備一個初始化檔案 .ini,當插入模組時 akvcam 會抓取這個檔案來判斷需要建什麼裝置和功能。以下提供一個正確的初始化檔案
接著編譯專案
插入模組(需要傳遞參數,將剛剛建立的 config 檔案傳入)
如果順利的話你將會在 /dev
底下發現新增了兩個 video device。
在資料夾底下準備一個範例影片 small.webm,透過 ffmpeg 來傳入 output device。
接著透過 vlc 打開 capture device,則可看到傳入的影片 small.webm