# 2024q1 Final Project (vcam)
contributed by < `yinghuaxia` >
Reference:
* [2020 年開發紀錄](https://hackmd.io/@eecheng/B16rQ3GjU) / [GitHub](https://github.com/eecheng87/vcam)
* [2021 年開發紀錄](https://hackmd.io/@WayneLin1992/HkDBmLUDO) / [GitHub](https://github.com/WayneLin1992/vcam)
* [2022 年開發紀錄](https://hackmd.io/@Masamaloka/linux2022-vcam) / [GitHub](https://github.com/kevinshieh0225/vcam)
* [vcam 測試記錄](https://hackmd.io/@bentu/SkUFns9XI?fbclid=IwAR0dip-LYX8zLmaOWMcCdEu5w8fCWPtjBLh9TV7vH0FRQG1jmfNboDlIvI4#%E6%B8%AC%E8%A9%A6-vcam)
* [hungyuhang 開發紀錄](https://hackmd.io/7-XO3QBHRYyXT6Qmn68vPw?view)
* [Github](https://github.com/yinghuaxia/vcam)
- [x] 環境建置
- [x] yuyv
- [x] trace code
> TODO: vcam
TODO: 看 https://hackmd.io/@Masamaloka/linux2022-vcam 並紀錄問題
TODO: Ubuntu Linux 24.04 (w/ Linux v6.8)
:::warning
TODO 事項及後續更動將更新於 [2024 期末專題](https://hackmd.io/@sysprog/S1l3ZlcLA) 中
:::
### vcam 運作原理
**v4l2 (Video4Linux2)**
[v4l2](https://android.googlesource.com/kernel/msm/+/android-msm-bullhead-3.10-marshmallow-dr/Documentation/video4linux/v4l2-framework.txt) 是 Linux 下關於影像收集相關裝置的驅動框架,也是一個統一的介面規範。其架構如下圖所示:
![image](https://hackmd.io/_uploads/HkgFnchI8A.png)
:::danger
用 Graphviz 重新繪製圖片,避免品質低劣的圖表
:::
Reference: [第一章 v4l2 簡介](https://work-blog.readthedocs.io/en/latest/v4l2%20intro.html#fig-v4l2-arch)
從架構圖中,我們可以將 v4l2 分為三個層級,分別為 User Space、Kernel Space 和 Hardware space。在 User Space 的包含 `/dev/videoX` 模組;在 Kernel Space 的包含 `cdev`、`video_device`、`v4l2 device` 及 `v4l2_subdev` 模組;在 Hardware space 的包含 `Camera sensor hardware` 模組。而當中的參數設置主要是透過 `ioctl` 進行設定的。
* `/dev/videoX`
User space 是使用者可以存取到的 device 節點,在圖中為 `/dev` 底下的 videoX,從 Kernel space 來看,這個 device 的主裝置編號及次裝置編號分別為 81 和 X。
* 其連接到的 `cdev` 是個由字符驅動的核心模組,功能為連接 `video_device` 和 `/dev` 底下的 device 節點(此圖中 `/dev/videoX`),這個模組讓 User space 的使用者可以透過一些操作(像是 open、 read、 write 等)來存取 `video_device`。
* 與 `cdev` 連接的 `video device` 主要是負責影像裝置的模組,他將影像裝置的各種功能(像是 open、read、write 等)提供給上層應用的介面,從 `v4l2_subdev` 中獲得影像的資料和他的控制訊息。
* `v4l2_subdev` 代表的是一個 V4L2 的裝置,透過這個模組,應用程式就可以配置及控制影像裝置。而 `video_device` 可以通過 `v4l2_subdev` 和具體的子裝置 (如攝影機感測器) 進行溝通。
* `Camera Sensor Hardware` 是實際的攝影機感測器硬體,可以實際的捕捉影像的資料。
:::info
:bulb: 在 Linux Kernel 中,裝置驅動程序分為三種類型:字元裝置(Character Devices)、塊裝置(Block Devices)和網路裝置(Network Devices)。這些裝置各自有不同的功能和應用場景,而 `cdev` 模組為字元裝置提供了基本的操作介面,並將字元裝置與內核中的設備號關聯起來。字元裝置是一種按字元為單位進行存取的裝置,使用者可以透過 read 和 write 等指令與裝置進行交互。常見的字元裝置包括鍵盤、滑鼠等,這些裝置的共同特徵就是每次操作時,電腦都會直接進行處理(像是按下滑鼠的按鍵、鍵盤輸入一個字元等)。
:::
從==使用者==的角度來對 V4L2 控制流程做觀察:
1. 使用者使用 [open()](https://man7.org/linux/man-pages/man2/open.2.html) 在 US(User Space) 通過 `/dev/videoX` 存取 影像裝置。並可以使用 [ioctl](https://man7.org/linux/man-pages/man2/ioctl.2.html) 來查詢裝置相關的參數設定,並進行裝置的配置。
2. 協商 (Negotiate) 資料格式
3. 協商 (Negotiate) 輸入 / 輸出方式,像是:
* read / write: 應用程式透過 read 和 write 的系統來讀取和寫入影像的資料,如第一點所提到的,應用程式會先使用 `open()` 打開 `/dev/video0` 裝置文件,透過 read 就可以呼叫裝置讀取資料,透過 write 就可以呼叫裝置寫入資料。
* Stream I / O: 這個方式允許應用程式自己分配自己的 buffer 區域,並將這些 buffer 區域的位址告訴驅動程式,驅動程式會直接將資料寫到用戶的 buffer 區域。
* overlay: 是一種 memory 的 mapping,驅動程式會將一組內核的 buffer 區域映射到 User space,這樣就可以讓使用者透過應用程式直接存取這些 buffer 區域進行讀寫操作。而應用程式使用的是 [mmap](https://man7.org/linux/man-pages/man2/mmap.2.html) 的巨集來對 buffer 區域進行存取。
4. 最後需要使用 [close()](https://man7.org/linux/man-pages/man2/close.2.html) 將資源釋放並將裝置關閉。
如果使用==編寫 driver==的角度對 V4L2 做觀察,編寫者只需要實作硬體相關的程式碼,並且註冊相關裝置即可,其餘部分 V4L2 已經完成設置。除了編寫具體裝置的程式碼之外,另一個主要的操作是將程式碼與 V4L2 的框架進行綁定,包含將我們的結構與 V4L2 框架中相關結構綁定在一起,以及將 V4L2 所定義的空函數指針與自己的函數 `v4l2_device` 綁定。V4L2 中主要的結構體如下圖中橘色長方體所示:
![image](https://hackmd.io/_uploads/rkm4tNWIR.png)
如果要將我們的結構與 V4L2 框架中相關結構綁定在一起,我們需要先了解以下幾個重要的結構:
> 參考資料:
> [1.4 Video device's internal representation](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-dev.html)
* `struct video_device`:這是主要的裝置結構體,用來表示一個像 `/dev/videoX` 的 V4L2 裝置,負責向 kernel 註冊字元裝置以讓 User Space 的的應用程式可以透過標準的文件操作(如 `open`、`read`、`write`、`ioctl` )來與裝置交互。他包含指向裝置操作的指針( `v4l2_file_operations` 及 `v4l2_ioctl_ops` )、控制處理器的指針( `v4l2_ctrl_handler` )之外,還有一個親代裝置指針指向 `v4l2_device`。在 vcam 程式碼中配置 `video_deive` 空間的方式如下所示:
```c
struct video_device *vdev = video_device_alloc();
if (vdev == NULL)
return -ENOMEM;
vdev->release = video_device_release;
```
而開發者實際註冊一個 `video_device` 可以使用 [video_register_device](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-dev.html#c.video_register_device) 巨集;解除註冊則是使用 [video_unregister_device](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-dev.html#c.video_unregister_device)。註冊時的程式碼如下,這個步驟會在初始化裝置之後進行:
```c
err = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
if (err) {
video_device_release(vdev); /* or kfree(my_vdev); */
return err;
}
```
從程式碼中我們可以看到需要傳入的參數包含 `vdev`,也就是我們幫 `video_device` 配置的空間;`VFL_TYPE_GRABBER`,也就是這一個 `video_device` 的種類;第三個參數是這個 `video_device` 可以控制的節點編號,`-1` 代表讓 v4l2 framework 自行選擇第一個閒置的節點編號。
`video_device` 的種類包含:
* VFL_TYPE_GRABBER: /dev/videoX for video input/output devices
* VFL_TYPE_VBI: /dev/vbiX for vertical blank data (i.e. closed captions, teletext)
* VFL_TYPE_RADIO: /dev/radioX for radio tuners
* VFL_TYPE_SDR: /dev/swradioX for Software Defined Radio tuners
* VFL_TYPE_TOUCH: /dev/v4l-touchX for touch sensors
當要對 `video_device` 進行初始化時,需要將其 `v4l2_dev` 的成員指向已經初始化的 `v4l2_deice` 結構,以定義這個 `video_deive` 是被哪一個 `v4l2_devive` 所管理。
* `struct v4l2_device`:一個更高階的裝置結構體,負責管理多個子裝置。其中包含一個指向 `v4l2_ctrl_handler` 的指針,也有一個 `list_head` 用來鏈接所有的子裝置。
在 vcam 專案中的 `device.c` 中可以找到初始化一個 `v4l2_deice` 的程式碼,使用的是 `v4l2_deice_register` 來進行初始化的動作:
```c
/* Register V4L2 device */
snprintf(vcam->v4l2_dev.name, sizeof(vcam->v4l2_dev.name), "%s-%d",
vcam_dev_name, (int) idx);
ret = v4l2_device_register(NULL, &vcam->v4l2_dev);
if (ret) {
pr_err("v4l2 registration failure\n");
goto v4l2_registration_failure;
```
* `struct v4l2_subdev`:具體的子裝置結構體,包含多種操作 ( `v4l2_subdev_ops` ),而這些操作會再被進一步劃分為不同的類別。
上面三個結構體均包含 [v4l2_ctrl_handler](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-controls.html#objects-in-the-framework) 的指針,這是一個負責處理裝置的控制命令,負責持續追蹤控制操作,其包含一個他擁有的 `v4l2_ctrl` 物件清單,代表具體的控制項,像是裝置的類型、名稱、步長等訊息。透過 `v4l2_strl_handler`,驅動程式可以統一管理裝置的控制項,處理來自 User Space 的控制命令,並將這些轉換成對於硬體的具體操作。
### Framebuffer 簡介
> 參考資料:
> [1.13 Video Framework](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-videobuf.html)
> [Wikipedia Framebuffer](https://en.wikipedia.org/wiki/Linux_framebuffer)
Framebuffer 是 RAM 中的一部分,可以視為一個 V4L2 driver 和 User Space 的連接,專門儲存驅動影像播放的 bitmap,是一個包含影像中所有 pixel 的 memory buffer。當螢幕的顯示需要更新時,應用程式可以透過 I / O 技術將新的圖像資料寫入 framebuffer 的(主) 記憶體區域,顯示器則會從 framebuffer 中讀取資料並輸出到裝置上。
而並不是所有的裝置都使用相同的的 buffer,buffer 有三種常見的種類,我們可以依照需求選擇適當的 buffer 類型作為 framebuffer:
* Buffers which are scattered in both the physical and (kernel) virtual address. 幾乎所有的 User Space buffer 是這種類型,這種 buffer 的分配及使用較靈活,但這種類型的 buffer 會需要可以支持 scatter/gather [DMA](https://en.wikipedia.org/wiki/Direct_memory_access) 操作的硬體。
* Buffers which are physically scattered, but which are virtually contigous. 這種 buffer 可以被用於 DMA 操作不可用的情形,其 virtually contigous 的特性讓其相對較容易操作。
* Buffers which are physically contigous. 這種 buffer 會被使用在 DMA 沒有支援 scatter / gather 的功能時。
:::info
:bulb: Physical address 指的是實際的硬體(主) 記憶體地址。每個(主) 記憶體單元在(主) 記憶體芯片上都有一個唯一的位址,這些位址直接由(主) 記憶體控制器管理。當 CPU 存取(主) 記憶體時,實際上是通過 physical address 來讀寫(主) 記憶體資料。
:bulb: Virtual address 是操作系統為每個<s>行程</s> 提供的一個邏輯地址空間。這個邏輯地址空間與 physical address 空間分離,系統可以透過 page tables 將virtual address 映射到 physical address。這樣可以讓每個行程不需要關心 physical address 的分佈情況。
:::
使用以下表格統整三種 buffer 的使用時機,並概述 framebuffer 在甚麼時機應該選用哪種 buffer 類型:
| Buffer type | 適用情形 |
|:---------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
| Both scattered | 通常適用於高性能的 video captured 裝置,這些裝置可以處理分散的(主) 記憶體區塊。這種方式在(主) 記憶體碎片較多的系統中特別有用,因為它不需要大塊連續的(主) 記憶體。 |
| Physically scattered, virtual contigous | 這類 buffer 在 kernel space 中的操作方便,適合那些需要 kernel 層面進行大量處理的影像應用,例如影片解碼或圖像處理等。但由於硬體位址上不連續,不適合簡單的 DMA 操作。 |
| Physically contigous | 這是傳統 video captured 和顯示裝置最常用的 buffer 類型,特別是在 DMA 操作必須非常高效的情況下。這些 buffer 確保資料在硬體和(主) 記憶體之間的快速傳輸,但在(主) 記憶體管理上需要更加小心以避免碎片問題。 |
### vcam 的 Colorspace
> 參考資料:
> [2.15 Defining Colorspaces in V4L2](https://www.kernel.org/doc/html/v4.15/media/uapi/v4l/colorspaces-defs.html)
> [Video4Linux2 part 5a: colors and formats](https://lwn.net/Articles/218798/)
Colorspace 的意思是用來描述顏色的座標空間,V4L2 中定義了不同種類的 colorspace,但只有兩種較常被使用,分別是
1. `V4L2_COLORSPACE_SRGB`:使用 [red, green, blue] tuple 來儲存顏色,這個 colorspace 同時也可與 [YUV](https://zh.wikipedia.org/zh-tw/YUV) 和 [YCbCr](https://en.wikipedia.org/wiki/YCbCr) colorspace 做轉換。
2. `V4L2_COLORSPACE_SMPTE170M`:用來表示類比訊號,常用於 NTSC 或 PAL 電視中。
在 vcam 驅動中需要使用不同的 colorspace 的原因是因為要確保虛擬相機能夠更正確的模擬真實裝置的顏色和顯示。根據具體的應用需求和顯示裝置選擇適當的 colorspace,可以提高顏色準確性和一致性,滿足不同應用場景的需求。
### vcam 的 I / O
> 參考資料:[Video4Linux2 part 4: inputs and outputs](https://lwn.net/Articles/213798/)
在 V4L2 的驅動中,I / O 負責處理應用程式與驅動程序間的資料傳輸。在很多情況下,一個 video adapter 不會提供多種 I / O 的選項,比如說相機控制器可以提供相機介面,但幾乎沒有提供其他選項。有一些較複雜的 video adapter,像是電視卡,會有多個輸入介面來連接到不同的連接器,這些不同的介面與對應的調節器 ([tuner](https://en.wikipedia.org/wiki/TV_tuner_card)) 可以讓輸入的介面有不同的特性。
:::info
:bulb: Video adapter 指的是在電腦中的圖形卡,用來連接顯示器與電腦,讓電腦可以輸出文字或是影像到顯示器上,通常由 GPU (圖形處理單元)、顯存 (VRAM)、輸出介面 (像是 HDMI、VGA 等) 和散熱系統組成。他的工作流程為:
1. CPU 將要渲染的圖形資料傳送到 GPU,GPU 負責執行渲染的動作。
2. GPU 將這些渲染過的圖像資料儲存在顯存當中,這些圖像相關的資料組成最終顯示的圖像。
3. 最後再將顯存中儲存的圖像資料通過輸出介面轉換成影像的訊號,傳送到顯示裝置上進行顯示。
:::
以下對輸入命令進行說明:
* 使用 `VIDIOC_ENUMINPUT` 將所有的輸入源列出
```c
int (*vidioc_enum_input)(struct file *file, void *private_data,
struct v4l2_input *input);
```
`file` 對應的是要開啟的影像裝置,`private_data` 指的是 driver 設定的 private field,`input` 則是真實資訊通過的區域,其中列出幾點:
* `__u32 index`: 唯一一個可以在 User Space 設置的參數,指的是使用者有興趣的 index number inputs。
* `__u8 name`: input 的名稱。
* `__u32 type`: input 的類別。
* `__u32 audioset`: 描述哪一個音訊的 input 可以與影像的 input 結合。
* `__u32 status`: 印出 input 的狀態。
### vcam 測試紀錄
測試環境:
```shell
$ uname -a
Linux yinghuaxia-Altos-P30-F6 6.5.0-35-generic #35~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue May 7 09:00:52 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
```
在執行 `make` 時遇到以下問題:
```shell=
yinghuaxia@yinghuaxia-Altos-P30-F6:~/vcam$ make
make -C /lib/modules/6.5.0-35-generic/build M=/home/yinghuaxia/vcam modules
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-35-generic'
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
CC [M] /home/yinghuaxia/vcam/module.o
CC [M] /home/yinghuaxia/vcam/control.o
In file included from ./include/linux/linkage.h:7,
from ./arch/x86/include/asm/cache.h:5,
from ./include/linux/cache.h:6,
from ./arch/x86/include/asm/current.h:9,
from ./include/linux/sched.h:12,
from ./include/linux/ratelimit.h:6,
from ./include/linux/dev_printk.h:16,
from ./include/linux/device.h:15,
from /home/yinghuaxia/vcam/control.c:3:
/home/yinghuaxia/vcam/control.c: In function ‘create_control_device’:
./include/linux/export.h:29:22: error: passing argument 1 of ‘class_create’ from incompatible pointer type [-Werror=incompatible-pointer-types]
29 | #define THIS_MODULE (&__this_module)
| ~^~~~~~~~~~~~~~~
| |
| struct module *
/home/yinghuaxia/vcam/control.c:303:38: note: in expansion of macro ‘THIS_MODULE’
303 | ctldev->dev_class = class_create(THIS_MODULE, dev_name);
| ^~~~~~~~~~~
In file included from ./include/linux/device.h:31:
./include/linux/device/class.h:230:54: note: expected ‘const char *’ but argument is of type ‘struct module *’
230 | struct class * __must_check class_create(const char *name);
| ~~~~~~~~~~~~^~~~
/home/yinghuaxia/vcam/control.c:303:25: error: too many arguments to function ‘class_create’
303 | ctldev->dev_class = class_create(THIS_MODULE, dev_name);
| ^~~~~~~~~~~~
./include/linux/device/class.h:230:29: note: declared here
230 | struct class * __must_check class_create(const char *name);
| ^~~~~~~~~~~~
cc1: some warnings being treated as errors
make[3]: *** [scripts/Makefile.build:251: /home/yinghuaxia/vcam/control.o] Error 1
make[2]: *** [/usr/src/linux-headers-6.5.0-35-generic/Makefile:2039: /home/yinghuaxia/vcam] Error 2
make[1]: *** [Makefile:234: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-35-generic'
make: *** [Makefile:14: kmod] Error 2
```
從第 35 行中,我們可以看到 `class_create` 這個函式只接受一個參數,且該參數為 `const char *` 類別,但我們從第 31 行的錯誤訊息中可以看到 `too many arguments to function 'class_create'` 且在 `control.c` 中的 `ctldev->dev_class = class_create(MY_MODULE, dev_name)` 中傳入了兩個參數,其中 `MY_MODULE` 在錯誤訊息第 20 行中是 `struct module *`,`dev_name` 是呼叫 `create_control_device` 時傳入的 `const char *` 類別,因此推測不應該出現的參數為 `MY_MODULE`。將 `control.c` 檔案中的 `ctldev->dev_class = class_create(MY_MODULE, dev_name);` 中的 `MY_MODULE` 刪除。
```diff
- ctldev->dev_class = class_create(THIS_MODULE, dev_name);
+ ctldev->dev_class = class_create(dev_name);
```
:::danger
將心比心,你希望被人叫做「有人」嗎?明確標注 GitHub 帳號名稱。
:::
在 [vcam](https://github.com/sysprog21/vcam) 的專案上 [hungyuhang](https://github.com/hungyuhang) 也已經提交變更:[vcam commit a7e6f0e](https://github.com/sysprog21/vcam/commit/a7e6f0ea18ee3eb49e3db9649ca5b94cc7f2563b)
重新執行 `make` 命令,會發生以下錯誤:
```shell
make -C /lib/modules/6.5.0-35-generic/build M=/home/yinghuaxia/vcam modules
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-35-generic'
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
CC [M] /home/yinghuaxia/vcam/control.o
CC [M] /home/yinghuaxia/vcam/device.o
CC [M] /home/yinghuaxia/vcam/videobuf.o
CC [M] /home/yinghuaxia/vcam/fb.o
/home/yinghuaxia/vcam/fb.c: In function ‘vcamfb_open’:
/home/yinghuaxia/vcam/fb.c:22:31: error: implicit declaration of function ‘PDE_DATA’; did you mean ‘NODE_DATA’? [-Werror=implicit-function-declaration]
22 | struct vcam_device *dev = PDE_DATA(ind);
| ^~~~~~~~
| NODE_DATA
/home/yinghuaxia/vcam/fb.c:22:31: warning: initialization of ‘struct vcam_device *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
/home/yinghuaxia/vcam/fb.c: In function ‘vcamfb_release’:
/home/yinghuaxia/vcam/fb.c:44:31: warning: initialization of ‘struct vcam_device *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]
44 | struct vcam_device *dev = PDE_DATA(ind);
| ^~~~~~~~
cc1: some warnings being treated as errors
make[3]: *** [scripts/Makefile.build:251: /home/yinghuaxia/vcam/fb.o] Error 1
make[2]: *** [/usr/src/linux-headers-6.5.0-35-generic/Makefile:2039: /home/yinghuaxia/vcam] Error 2
make[1]: *** [Makefile:234: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-35-generic'
make: *** [Makefile:14: kmod] Error 2
```
在 [extensions: replace \`PDE_DATA\`](https://lore.kernel.org/all/20220204132643.1212741-2-jeremy@azazel.net/) 中提到:
```
The `PDE_DATA` function for retrieving private data from a procfs inode
has been replaced by `pde_data` in 5.17. Replace all instances of the
former with the latter, but add a macro to xtables_compat.h in order to
preserve compatibility with older kernels.
```
而在 [proc: remove PDE_DATA() completely](https://lore.kernel.org/all/20220122061423.HMq1py5Q0%25akpm@linux-foundation.org/) 也有寫到以下的更動:
```
Remove PDE_DATA() completely and replace it with pde_data().
```
因此將 `fb.c` 中的 `PDE_DATA` 改成 `pde_data` 即可解決此問題。
接著執行 `$ sudo ./vcam-util -l` 可以得到以下結果:
```shell
Available virtual V4L2 compatible devices:
1. fb1(640,480,rgb24) -> /dev/video0
```
查看 `vcam-util.c` 檔案,`-l` 命令會執行 `list_devices()` 函式,先使用 [ioctl](https://man7.org/linux/man-pages/man2/ioctl.2.html) 函式來獲得現在有的裝置訊息,並將結果存在 `dev` 中,隨後將 `dev.idx`, `dev.fb_node`, `dev.width`, `dev.pix_fmt` 以及 `dev.video_node` 的結果印出。`dev` 的架構如下所示:
```c
struct vcam_device_spec {
unsigned int idx;
unsigned short width, height;
pixfmt_t pix_fmt;
char video_node[64];
char fb_node[64];
};
```
內容包含索引值、framebuffer 的節點、虛擬攝影機裝備的寬度與高度(此虛擬裝置的解析度)、像素的格式 (rgb24 或 yuyv) 以及影片的節點。
透過 `ls /dev/video*` 來確認具體的裝置號為 `/dev/video0` 以進行後續對 driver 的測試:
```shell
yinghuaxia@yinghuaxia-Altos-P30-F6:~/vcam$ ls /dev/video*
/dev/video0
```
[v4l2-compliance](https://manpages.ubuntu.com/manpages/bionic/man1/v4l2-compliance.1.html) 命令是用來測試 video4linux 裝置(包含輸入及輸出)的命令,且幾乎覆蓋到全部的 v4l2 ioctl。這個命令測試的不僅是裝飾是否遵守 v4l2 API,也測試了 driver 是否使用正確的框架。如果使用者想要提交一個新的 v4l2 driver,那該 driver 必須通過 `c4l2-compliance` 的命令且不出錯。以下針對幾個常用的命令參數進行解說:
```
-d, --device=<dev>
Use device <dev> as the video device. If <dev> is a number, then /dev/video<dev> is used.
-f, --stream-all-formats
Test whether all available formats can be streamed. This attempts to stream using MMAP mode or read/write (if V4L2_MEMORY_MMAP is not available) for one second for all formats, at all sizes, at all intervals and with all field values. In addition, if the driver supports scaling, cropping or composing it will test that as well in various combinations. If the driver supports a lot of combinations then this test can take a long time.
The configuration of the driver at the time v4l2-compliance was called will be used for the streaming tests.
-s, --streaming=<count>
Enable the streaming tests. Set <count> to the number of frames to stream (default 60). This requires that before v4l2-compliance is called the device has been configured with a valid input (or output) and frequency (when the device has a tuner). For DMABUF testing --expbuf-device needs to be set as well.
The configuration of the driver at the time v4l2-compliance was called will be used for the streaming tests.
```
以下為執行此命令的結果,旨在測試 video0 所有可使用的格式是否可以被串流:
```shell
$ sudo v4l2-compliance -d /dev/video0 -f
...
Total for vcam device /dev/video0: 47, Succeeded: 47, Failed: 0, Warnings: 0
```
[v4l2-ctl](https://manpages.ubuntu.com/manpages/xenial/man1/v4l2-ctl.1.html) 則是控制 video4linux 裝置的命令,以下為幾個常用參數:
```
-d, --device=<dev>
Use device <dev> as the V4L2 device. If <dev> is a number, then /dev/video<dev> is used.
-w, --wrapper
Use the libv4l2 wrapper library for all V4L2 device accesses. By default v4l2-ctl will directly access the V4L2 device, but with this option all access will go via this wrapper library.
--all Display all information available.
```
執行 `$ sudo v4l2-ctl -d /dev/video0 --all` 命令的結果:
```shell
v4l2-compliance 1.22.1, 64 bits, 64-bit time_t
Compliance test for vcam device /dev/video0:
Driver Info:
Driver name : vcam
Card type : vcam
Bus info : platform: virtual
Driver version : 6.5.13
Capabilities : 0x85200001
Video Capture
Read/Write
Streaming
Extended Pix Format
Device Capabilities
```
在掛載 `vcam.ko` 的前後可以使用 `ls /dev` 命令來觀察掛載前後模組的差異。在我的裝置中,掛載後多了 `video0` 和 `fb1` 等,因此再使用 `$ sudo fbset -fb /dev/fb1 --info` 命令來觀察 framebuffer `fb1` 的資訊。
```shell
mode "640x480"
geometry 640 480 640 480 24
timings 0 0 0 0 0 0 0
rgba 8/0,8/8,8/16,0/0
endmode
Frame buffer device information:
Name : vcamfb
Address : 0xffff9f1346979000
Size : 921600
Type : PACKED PIXELS
Visual : TRUECOLOR
XPanStep : 1
YPanStep : 1
YWrapStep : 1
LineLength : 1920
Accelerator : No
```
### Code trace
**module.c**
這份檔案主要功能在於在掛載模組時建立控制裝置和指定數量的虛擬攝影機裝置,並在模組卸載時清理資源。其中,`device_max`、`create_devices`、`allow_pix_conversion` 等是模組的參數,讓使用者可以在加載模組的時候設置。`vcam_init` 負責模組的初始化,根據 `create_control_device` 參數的值建立虛擬攝影機裝置。
**control.c**
這份檔案主要功能在於處理控制裝置的相關操作,提供 User Space 和 Kernel Space 的介面,由以下的程式碼可見一斑:
```c
struct control_device {
int major;
dev_t dev_number;
struct class *dev_class;
struct device *device;
struct cdev cdev;
struct vcam_device **vcam_devices;
size_t vcam_device_count;
spinlock_t vcam_devices_lock;
};
```
**device.c**
這個檔案負責虛擬攝影機的核心功能。開頭的 `pr_fmt(fmt) KBUILD_MODNAME ": " fmt` 定義印出的格式,用來調試信息。影像裝置操作函式包含:
* `vcam_querycap`: 查詢裝置的能力。
* `vcam_try_fmt_vid_cap`: 設定影片的格式,並檢查支持性。
* `vcam_enum_frameintervals`: 枚舉支援的 frame 間隔。
* `vcam_g_fmt_vid_cap` 和 `vcam_s_fmt_vid_cap`: 獲得和設置目前的影像格式。
**fb.c**
這個檔案負責處虛擬相機的 framebuffer 相關操作,`vcamfb_init` 將 buffer 做初始化,動作包含(主) 記憶體的分配、參數的設置以及對 buffer 進行註冊 (`register_framebuffer`)。Framebuffer 可操作的函式包含 `vcam_fb_setcolreg` (顏色暫存器設置)、`vcam_fb_check_var` (檢查並設置螢幕訊息的變數)、`vcam_fb_mmap` ((主) 記憶體映射)等。
**vcam-util.c**
這個檔案的功能在於操作虛擬相機裝置的 User Space 工具。其中定義了對虛擬相機的不同操作與裝置規格初始化(`struct vcam_device_spec`),操作包含 `ACTION_CREATE`、`ACTION_DESTROY`、`ACTION_MODIFY` 等。提供解析度和 pixel 格式的函式為 `parse_resolution` 和 `determine_pixfmt`。主函式 `main` 中可以根據命令列選項的設定和操作裝置,來處理建立、移除、修改和列出裝置等請求。
**videobuf.c**
這個檔案的功能在於實作與 video buffer 相關的操作,主要是利用 [Videobuf2(VB2)](https://www.kernel.org/doc/html/v4.10/media/kapi/v4l2-videobuf2.html) 框架來對影片資料的 buffer 來進行管理。`vcam_out_queue_setup` 負責檢查 buffer 的數量及大小等基本參數;`vcam_out_buffer_prepare` 檢查 buffer 大小是否足夠儲存影片的<s>資料</s>,並設置 buffer 的有效負載大小;`vcam_start_streaming` 和 `vcam_stop_streaming` 負責啟動及停止 kernel 的 thread,確保影片的正常運行及清空 buffer;`vcam_outbuf_lock` 和 `vcam_outbuf_unlock` 會鎖定 / 解鎖 buffer,以確保 multi-thread 操作的安全性。
### 使用 vcam driver
在完全沒有寫入 vcam 的 framebuffer 時,用 `vlc v4l2:///dev/video0` 顯示的預設畫面如下所示,為一個漸層的畫面:
![image](https://hackmd.io/_uploads/Sk4fd_b4A.png)
如果我們想要對 vcam 所呈現的畫面做更動時,我們要將程式碼寫入其對應的 framebuffer 中。根據 [2020 開發紀錄](https://hackmd.io/@eecheng/B16rQ3GjU#%E6%B7%B1%E5%85%A5%E6%93%8D%E4%BD%9C),我們可以先建立一個檔案 `test.c` ,當中是我們想要對 framebuffer 所呈現畫面進行的操作,對該檔案進行編譯之後,使用 `./test > /proc/vcamfb` 將檔案寫入 vcam 對應的 framebuffer 中。然而會出現關於沒有 `/proc/vcamfb` 的報錯,當我們將 `/proc/` 底下的檔案列出後,發現當中有一個 `fb` 檔案,對其執行 `cat /proc/fb` 的命令來查看內容時發現 `vcamfb` 其實存在:
```shell
$ cat /proc/fb
0 i915drmfb
1 vcamfb
```
這個結果表示系統中有兩個 framebuffer 的裝置,包含 `i915drmfb` 和 `vcamfb`。
更仔細的去探究兩個檔案之間的差異,發現 [/proc](https://man7.org/linux/man-pages/man5/procfs.5.html) 是一個虛擬的檔案系統,其提供一個界面,讓使用者獲得系統相關的訊息,當中大部分的檔案是唯讀的。而其中的 `/proc/fb` 檔提供當 kernel 被編譯時所定義的 framebuffer 的相關資訊。[/dev](https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/dev.html) 則是各種 device 儲存的位置,我們可以透過以下命令查看 `/dev` 下的 `fb0` 和 `fb1` 對應到 `/proc/fb` 中的哪一個裝置名稱:
```shell
$ cat /sys/class/graphics/fb0/name
i915drmfb
$ cat /sys/class/graphics/fb1/name
vcamfb
```
因此我們可以確認 `fb1` 在 `/proc/fb` 底下對應到的是 `vcamfb` 這個名稱。我們重新改寫命令為 `$ sudo sh -c './test > /dev/fb1'`,將想要對 vcam 畫面做的操作寫入 `fb1` 中,就可以成功的將想進行的操作呈現:
![rgb](https://hackmd.io/_uploads/Byh4YeXNA.gif)
:::danger
注意用語!
* memory 是「(主) 記憶體」,而非「內存」
* implement 是「實作」
* device 是「裝置」,而非「設備」
* interface 是「介面」,而非「接口」
* data 是「資料」,而非「數據」
* create 是「建立」,而非「創建」
* process 是「行程」,而非「進程」
* access 是「存取」,而非「訪問」(visit)
* call 是「呼叫」,而非「調用」
* character device 是「字元裝置」,而非「字符設備」
* command 是「命令」,而非「指令」(instruction)
參見 [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries) 和 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
> 已更改
:warning: 不!你沒做到 (例如「字符」的誤用),務必詳細閱讀[詞彙對照表](https://hackmd.io/@l10n-tw/glossaries) 和 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary),隨後進行用語調整,尊重台灣資訊科技前輩的篳路藍縷。
:::