# Linux 核心專題: vcam 研究
> 執行人: padaray
> [專題解說影片](https://youtu.be/OVUKaIWNOFA)
### Reviewed by `ollieni`
>現代使用 Framebuffer 流程:
CPU 將定義好的圖形算繪指令傳送給 GPU
GPU 解析渲染指令,渲染後將生成的影像資料寫入 Framebuffer 中,通常以 bitmap 格式
video card 讀取 Framebuffer,生成影像訊號,傳送給顯示器
(螢幕)
疑問 : 請問如何生成影像訊號?
## 任務簡介
投入 vcam 開發,指出其他學員成果的不足和謬誤,予以建議,開發紀錄務必依據[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)和[詞彙對照表](https://hackmd.io/@l10n-tw/glossaries)。
## TODO: vcam 開發
> 確保在 Linux v6.8+ 運作。
### 整理過去報告
根據 2020 年到 2023 年的開發紀錄,將有提到的概念彙整,並且加入自己的理解。
* [2020 年開發紀錄](https://hackmd.io/@eecheng/B16rQ3GjU)
* [2021 年開發紀錄](https://hackmd.io/@WayneLin1992/vcam)
* [2022 年開發紀錄](https://hackmd.io/@Masamaloka/linux2022-vcam)
* [2023 年開發紀錄](https://hackmd.io/@sysprog/rJEhcgoSn)
### **Video for Linux 2 (V4L2)**
主要參考自:
[2022 年開發紀錄](https://hackmd.io/@Masamaloka/linux2022-vcam)
[The Video4Linux2 API: an introduction](https://lwn.net/Articles/203924/) - 此文件版本較舊,函式或變數名稱很多都不同,但可以用來了解整個流程
[Linux Kernel’s documentation: V4L2](https://www.kernel.org/doc/html/v4.11/media/kapi/v4l2-intro.html#structure-of-the-v4l2-framework) - Linux Kernel 的官方文件,完整介紹函式如何使用
[Linux V4L2 學習](https://work-blog.readthedocs.io/en/latest/index.html)
:::danger
V4L2 涵蓋的範圍已不只「影音管理」,請依循 Linux 核心原始程式碼裡頭對應的 Kconfig 來更新其描述。
:::
V4L2 是 Linux 核心的<s>影音管理 API</s> ,主要功能是負責支援各種裝置 (device),只有一些是關於影像
> V4L2 is designed to support a wide variety of devices, only some of which are truly "video" in nature. - [The Video4Linux2 API: an introduction](https://lwn.net/Articles/203924/)
#### 為甚麼需要 V4L2 的存在 ?
為 Linux Kernel 提供一個**統一的 API** ,不同的 `video capture interface` (輸入) 會有不同的硬體驅動需求,我們透過 V4L2 這個 API 則可以直接和不同的硬體裝置互動。V4L2 也提供多種影像格式和解析度 (可調參數),滿足各個應用程式的需求。
#### V4L2 整體框架
:::info
在使用 V4L2 函式前,必須要了解 video_device、v4l2_device、v4l2_subdev 他們之間的關係,如果直接從程式碼下去看,很容易造成觀念混亂,例如我搞混了 v4l2_device_register 和 video_device_register,以為它們是兩個相同的函式,只是版本不同
:::
![image](https://hackmd.io/_uploads/Hk8FWvrLR.png =80%x)
- v4l2 device number(81, X) : V4L2 的 major device number 是 81,透過存取 `/dev/videoX` 節點來使用 video device,例 : `mknod /dev/video0 c 81 0`
- character device driver : V4L2 本身是一個 character device[註1],因此需要透過此驅動程式運作,連接 `/dev/videoX` 和 video_device
- video_device : 裝置的具體操作方法,和 v4l2_subdev 進行互動
- v4l2_device : 可以看做是一個介面,管理很多個子裝置,將 video_device 和 v4l2_device 做連結
- v4l2_subdev : 實際上運作的各個裝置,實作具體功能,例如:sensor、camera
:::danger
character device 簡稱 chardev,但不代表這是「由 char 組成的裝置」,其本質就是 character (字元) 為基本操作單位,可對應到 CS:APP 的第 8 章。
:::
註1 : Linux 將裝置分為 character device、block device、network device,character device 顧名思義是一種由 <s>char</s> 組成的裝置,這種裝置就像 <s>文件</s> 一樣,透過依序讀取來實作資料流的概念,大部分都須實作 open、close、read、write 四種函式
:::danger
注意用語。
:::
#### 以使用者角度,如何使用 V4L2
對 user 來說 Kernel Space 實際上怎麼操作並不重要,他們只需要知道要使用 vcam,就存取 `/dev/videoX` 進行 open、close、ioctl...等操作即可
:::danger
注意書寫規範:
* 程式碼註解不該出現中文,總是用美式英語書寫
* 用流暢的漢語書寫
device 譯作「裝置」,不是「設備」。既然這是行之有年的詞彙,避免過多的中英交雜。
:::
**流程:**
1. 使用 `open` 函式**開啟 device**
```c
int open(const char *device_name, int flags)
// 實際應用
fd = open("/dev/video0", O_RDWR)
```
- device_name : 要開啟的 device
- flags : open flags,通常使用 O_RDWR 只進行讀寫模式
- return : file description,負數代表錯誤發生
2. 使用 `ioctl` 函式,**設定 device 的屬性**,選擇影像格式,設定資料格式
```c
int ioctl(int fd, int request, void *argp)
// VIDIOC_QUERYCAP request: 此裝置是甚麼以及此裝置能做甚麼
struct capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap)
```
- file:描述此裝置的文件,也就是 fd(`open`回傳值)
- request:依據不同的操作填入 request
- argp:不同的操作填入不同的參數
```c
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
```
3. 設定 I/O 方式 (暫存區的管理)
a. Read/Write : 默認方式,直接讀取和寫入文件
b. Stream I/O : 不 copy 資料,而是用流的方式傳輸
c. overlay : 將資料從 RAM copy 到 VRAM
4. 使用 `close` 函式**關閉 device**
```c
int close(int fd)
```
</br>
#### 以開發者角度,如何使用 V4L2
若想撰寫 driver 對 vcam 擴充功能,就需了解整個 Kernel Space 的運作,大致流程如下:
**將實際上運行的裝置連接到 V4L2**
- 宣告一個 `v4l2_device`
- 宣告並初始化一個以上的 `v4l2_subdev` ,綁定到 `v4l2_device`
**讓使用者可以透過 V4L2 操作 device**
- 宣告一個 `video_device` 實作 `v4l2_file_operations` 並用 `video_register_device` 註冊,完成就能連接 `video_device` 和 `v4l2_device`
- 通過將 `video_device` 指向 `v4l2_device` 完成連接, `cdev_add` 函式註冊 character device (讓 kernel 知道他是一個 character device),並且指定 `file_operations`,讓 user 可以使用 `open`/`read`/`write`/`ioctl` 方法
- `v4l2_device_register` 也會通過 `device_register` 讓其在 `/sys` 文件系統下可見,讓使用者可以看見並存取
結合上面的架構圖流程步驟如下,除了步驟 1 之外,其他步驟沒有一定順序:
![image](https://hackmd.io/_uploads/HkNIlbw8A.png =80%x)
使用者對裝置檔案的操作,核心會做 `file_operations` 的呼叫,例:呼叫 file_operations 的 open 函式
`v4l2_file_operations` 是 `file_operations` 的子集,目的是為了方便開發 V4L2,只取 `file_operations` 其中比較重要的操作,讓開發者可以專注在影像裝置的功能,並且達到開發的一致性
V4L2 通常會將 `file_operations` 指向 `v4l2_file_operations`,
:::danger
善用 Ftrace 追蹤執行流程。
:::
**1. 初始化 `v4l2_device`**
宣告 [`v4l2_device`](https://github.com/torvalds/linux/blob/master/include/media/v4l2-device.h),此函式定義在 `/include/media/v4l2-device.h`
```c
static struct v4l2_device my_v4l2_device;
ret = v4l2_device_register(NULL, &my_v4l2_device); //透過此方式初始化
void v4l2_device_get(struct v4l2_device *my_v4l2_device) //獲得該裝置的參數
```
```c
struct v4l2_device {
struct device *dev;
struct media_device *mdev;
struct list_head subdevs;
spinlock_t lock;
char name[36];
void (*notify)(struct v4l2_subdev *sd,
unsigned int notification, void *arg);
struct v4l2_ctrl_handler *ctrl_handler;
struct v4l2_prio_state prio;
struct kref ref;
void (*release)(struct v4l2_device *v4l2_dev);
};
```
**2. 初始化 `v4l2_subdev`,綁定 `v4l2_device`**
宣告 [`v4l2_subdev`](https://github.com/torvalds/linux/blob/master/include/media/v4l2-subdev.h#L1055),此函式定義在 `/include/media/v4l2-subdev.h`,使用 [`v4l2_device_register_subdev`](https://github.com/torvalds/linux/blob/master/include/media/v4l2-device.h#L159) 綁定 `v4l2_device`
```c
static struct v4l2_subdev my_subdev;
v4l2_subdev_init(&subdev, &my_subdev_ops);
ret = v4l2_device_register_subdev(&my_v4l2_device, &my_subdev)
```
**3. 宣告 `video_device` 實作 `v4l2_file_operations`**
```c
static struct video_device my_video_device;
static const struct v4l2_file_operations my_v4l2_fops = {
.owner = THIS_MODULE,
.open = open_func,
.release = release_func,
.read = read_func,
.write = write_func,
.unlocked_ioctl = video_ioctl2, // 通过 V4L2 框架处理 ioctl
};
```
**4. 呼叫 `video_register_device` 註冊 `video_device`**
使用在 `/include/media/v4l2-dev.h` 的 `video_register_device` 函式
```c
video_register_device(struct video_device *vdev, int type, int nr);
video_unregister_device(struct video_device * vdev)
ret = video_register_device(&my_video_device, VFL_TYPE_GRABBER, -1);
```
- vdev : 要註冊的 video_device
- type : 要註冊的裝置類型
- nr : 所需裝置的序號,0 = /dev/video0,-1 = first free
- return : 負數代表錯誤發生
**5. 通過將 `video_device` 指向 `v4l2_device` 完成連接**
```c
my_video_device.v4l2_dev = &my_v4l2_device;
```
**6. 驅動退出的時候註銷裝置**
```c
video_unregister_device(&my_video_device);
v4l2_device_unregister_subdev(&my_subdev);
v4l2_device_unregister(&my_v4l2_device);
```
### **Framebuffer**
vcam 會運用到 Framebuffer 的技術,以下資料整理自 [Wikipedia](https://en.wikipedia.org/wiki/Framebuffer) 、 過去開發紀錄 、 [Linux Kernel 文件](https://docs.kernel.org/fb/index.html)。
:::danger
「[幀](https://dict.revised.moe.edu.tw/dictView.jsp?ID=7954)」是量詞,而 frame 是名詞,應依據場景給予合適的譯詞。此處可譯作「影格」。
$\to$ [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries)
:::
**Frame** : 指的就是每一個影格,可以理解成一張圖片。我們常提到的 FPS (frame per second),就是每一秒鐘可以顯示多少張圖片
**buffer** : 一段記憶體空間,所擔任的角色是個暫存區,將待會要處理的資料先暫放在這
**Framebuffer** : 是 RAM (random-access memory) 的一部分[註1],將要處理的影像資料暫放的地方,之後 **vcam 就是從這裡取得 input**
[註1] : 這邊的 RAM 指的是 video card[註2] 中的 VRAM (Video RAM),用來儲存圖像資料
[註2] : [video card](https://zh.wikipedia.org/zh-tw/%E6%98%BE%E7%A4%BA%E5%8D%A1) 也稱作 graphics card,GPU 是其中的核心部分,其他部分有 VRAM、驅動程式、電路板
:::danger
依據中華民國教育部《重編國語辭典修訂本》對於「渲染」一詞的解釋如下:
1. 國畫的一種用色技巧。以水墨或顏料,襯托物像,使分出陰陽向背的效果。
2. 言詞、文字過度吹噓誇大。如:「新聞媒體常有渲染的報導。」
3. 一種電影創作的表現手法。它透過對景物、人物、環境的心理、行為,做多方面描寫形容,突出形象,加強藝術效果。
而英語的 render 一詞是「如實」地「展現」,無論是藉由色彩和光影著色表現,使其結果看來如實且具體,或者採用音樂一類的「表現方式」來詮釋。上述的《湯姆歷險記》中 render 唯一出現的句子,也是這樣的用法。反過來看「渲染」,這個具備多重意義的現代漢語詞彙,若要跟 render 的寓意映射,實在不恰當,喪失其「如實」的原則。我們要留意到,無論經由 GPU 抑或 CPU 進行 rendering 時,總是「輸入成分是什麼,輸出就該是什麼」,中間可沒「渲染」(無論是誇大或多方面描寫) 的成分!
於是 render 若用於電腦圖學領域,翻譯為「算繪」將是最適切的詞彙 —— 該領域的 render 就是「計算」加上「繪製」,忠實且精準的呈現。
參見: [論 Render 翻譯 (算繪/演繹)](http://breezymove.blogspot.com/2013/12/render.html)
:::
**現代使用 Framebuffer 流程**:
1. CPU 將定義好的圖形算繪指令傳送給 GPU
2. GPU 解析渲染指令,渲染後將生成的影像資料寫入 Framebuffer 中,通常以 bitmap 格式
3. video card 讀取 Framebuffer,生成影像訊號,傳送給顯示器 (螢幕)
:::danger
為何 vcam 用到 Linux framebuffer?
:::
### **程式碼理解**
:::danger
提供高階的描述,不是急著列出每個檔案、每個函式,避免「舉燭」。
:::
- **`module.c`**
主要透過 `vcam_init` 函式在掛載時初始化 vcam,在卸載時用 `vcam_exit` 清除記憶體。
`devices_max` 參數設定最多同時管理幾個 vcam
`create_devices` 設定初始化時建立的 vcam 數量
`allow_pix_conversion` 是否允許像素格式轉換
`allow_scaling` 是否可以縮放圖像
`allow_cropping` 是否可以裁減圖像
- **`control.c`**
此檔案主要負責建立和管理裝置,也實作了 charactor device 的介面,透過這個介面使用者只需要透過 User Space 的應用程式就可以和裝置進行互動,其中的函式都是和 `control_device` 結構互動,會在後面進行詳細介紹
- **`device.c`**
此檔案負責 ioctl 的操作實作、各種影像資料的處理、`v4l2_file_operations` 的實作,以下列舉:
`vcam_ioctl_ops` : 實作 iotcl 操作
`vcam_querycap` : 查詢裝置能做的事
`vcam_sizes[]` : 設定攝影機的解析度
`vcam_fops` : 定義了 open、release、read 等操作
`rgb24_to_yuyv`、`yuyv_to_rgb24`、`yuyv_to_rgb24_one_pix` : 實作各種格式的轉換
- **`videobuf.c`**
此檔案使用 `videobuf2` 框架來設置並管理 framebuffer,主要目的是將 framebuffer 的資料輸出,透過以下函式進行管理:
`vcam_out_queue_setup` : 根據影像大小,設置 buffer
`vcam_out_buffer_prepare` : 確定 buffer 足夠容納影像
`vcam_out_buffer_queue` : 將準備好的 buffer 添加到 queue
`vcam_start_streaming` : 啟動一個 thread 進行資料流傳輸
`vcam_stop_streaming` : 取消 thread 並停止資料流傳輸
`vcam_out_videobuf2_setup` : 初始化 videobuf2
`vcam_vb2_ops` : 定義操作 framebuffer 的操作,架構如下:
```c
static const struct vb2_ops vcam_vb2_ops = {
.queue_setup = vcam_out_queue_setup,
.buf_prepare = vcam_out_buffer_prepare,
.buf_queue = vcam_out_buffer_queue,
.start_streaming = vcam_start_streaming,
.stop_streaming = vcam_stop_streaming,
.wait_prepare = vcam_outbuf_unlock,
.wait_finish = vcam_outbuf_lock,
};
```
- **`fb.c`**
此檔案實作將裝置的資料輸入進 framebuffer,透過函式進行管理,以下為較為重要的函式:
`vcamfb_ops` : 將對 framebuffer 的操作綁定函式
`vcam_fb_open` : 打開 framebuffer 裝置
`vcam_fb_write` : 寫入 framebuffer
`vcam_fb_release` : 釋放 framebuffer 的記憶體(設為0),並且關閉 framebuffer 裝置
`vcam_fb_check_var` : 檢查調整 framebuffer 裝置的螢幕顯示資料
`vcam_fb_mmap` : 把 framebuffer 的記憶體空間 mapping 到 User space
`vcamfb_init` : 初始化 framebuffer 裝置
`vcamfb_destroy` : 釋放 framebuffer 的記憶體,註銷此裝置
- **`vcam-util.c`**
透過此檔案的函式,可以直接與 vcam 進行互動
`device_template` : 設定好預設的模板,沒有調整參數就用此模板
`create_device` : `ioctl` 函式傳送 `VCAM_IOCTL_CREATE_DEVICE` 請求,建立新的 vcam 裝置
`remove_device` : `ioctl` 函式傳送 `VCAM_IOCTL_DESTROY_DEVICE` 請求,移除特定索引值的 vcam 裝置
`modify_device` : `ioctl` 函式傳送 `VCAM_IOCTL_MODIFY_SETTING` 請求,調整 vcam 參數
`list_devices` : `ioctl` 函式傳送 `VCAM_IOCTL_GET_DEVICE` 請求,一個一個取得所有裝置資訊
### **VCAM 實作**
#### 版本
```shell
ray@ray:~$ uname -r
6.8.0-35-generic
```
#### make 遇到的問題
**1. `struct vb2_queue` 中的 `min_buffers_needed` 不存在**
```shell
ray@ray:~/vcam$ make
make -C /lib/modules/6.8.0-35-generic/build M=/home/ray/vcam modules
make[1]: Entering directory '/usr/src/linux-headers-6.8.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-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0
You are using: gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0
CC [M] /home/ray/vcam/module.o
CC [M] /home/ray/vcam/control.o
CC [M] /home/ray/vcam/device.o
CC [M] /home/ray/vcam/videobuf.o
/home/ray/vcam/videobuf.c: In function ‘vcam_out_videobuf2_setup’:
/home/ray/vcam/videobuf.c:138:6: error: ‘struct vb2_queue’ has no member named ‘min_buffers_needed’
138 | q->min_buffers_needed = 2;
| ^~
make[3]: *** [scripts/Makefile.build:243: /home/ray/vcam/videobuf.o] Error 1
make[2]: *** [/usr/src/linux-headers-6.8.0-35-generic/Makefile:1926: /home/ray/vcam] Error 2
make[1]: *** [Makefile:240: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-35-generic'
make: *** [Makefile:14: kmod] Error 2
```
**解決方式:**
修改 `/vcam/videobuf.c`,原因是在 Linux 6.8 中把 `min_buffers_needed` 重新命名成 `min_queued_buffers`
```diff
- q->min_buffers_needed = 2;
+ q->min_queued_buffers = 2;
```
**2. 修改問題 1 重新 make 後出現以下問題,沒宣告的 identifier**
```shell
ray@ray:~/vcam$ make
make -C /lib/modules/6.8.0-35-generic/build M=/home/ray/vcam modules
make[1]: Entering directory '/usr/src/linux-headers-6.8.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-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0
You are using: gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0
CC [M] /home/ray/vcam/videobuf.o
CC [M] /home/ray/vcam/fb.o
/home/ray/vcam/fb.c: In function ‘vcamfb_init’:
/home/ray/vcam/fb.c:405:19: error: ‘FBINFO_FLAG_DEFAULT’ undeclared (first use in this function); did you mean ‘FAULT_FLAG_DEFAULT’?
405 | info->flags = FBINFO_FLAG_DEFAULT;
| ^~~~~~~~~~~~~~~~~~~
| FAULT_FLAG_DEFAULT
/home/ray/vcam/fb.c:405:19: note: each undeclared identifier is reported only once for each function it appears in
make[3]: *** [scripts/Makefile.build:243: /home/ray/vcam/fb.o] Error 1
make[2]: *** [/usr/src/linux-headers-6.8.0-35-generic/Makefile:1926: /home/ray/vcam] Error 2
make[1]: *** [Makefile:240: __sub-make] Error 2
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-35-generic'
make: *** [Makefile:14: kmod] Error 2
```
**解決方式:**
:::danger
注意用語。
超連結通常對應到有明確標題的網頁,你該避免用「此篇」這種沒有資訊量可研的標示方式。
:::
參考 [此篇](https://github.com/DisplayLink/evdi/issues/452) 處理方式,文章中提到 [commit-b4f470aef449](https://github.com/sunflower2333/linux/commit/b4f470aef4492315190bca1dadc693f2a52b0fad) 將 `FBINFO_FLAG_DEFAULT` 移除,原因是 `FBINFO_FLAG_DEFAULT` 的值為 0 相當於不影響,而 `kzalloc` 已經做了初始化為 0。代表若用 `kzalloc` <s>分配</s> 空間後,就可以不需要再設置 `info->flags` 為 `FBINFO_FLAG_DEFAULT`,但在 `fb.c` 中原始碼下:
```c
fb_data = vmalloc(sizeof(struct vcamfb_info));
dev->fb_priv = (void *) fb_data;
info = &fb_data->info;
```
`fb_data` 是用 `vmalloc` 進行初始化,`vmalloc` 不會把記憶體位置初始化為 0,最簡單的方式是直接將 `info->flags` 值設為 0:
```diff
- info->flags = FBINFO_FLAG_DEFAULT;
+ info->flags = 0;
```
另外我選擇使用 `memset` 函式將此空間清除為 0,完成後即可 make,但非必要
```diff
fb_data = vmalloc(sizeof(struct vcamfb_info));
+ memset(fb_data, 0, sizeof(struct vcamfb_info));
```
以上兩個問題已經由 hungyuhang 發出了 [**pull request**](https://github.com/sysprog21/vcam/pull/39) 解決
:::danger
探討核心開發者變更程式碼的考量,並確認有無潛在的技術議題。
:::
### 安裝 vcam
依照 vcam [README](https://github.com/sysprog21/vcam/blob/master/README.md) 步驟安裝
**1. 安裝相關套件,方便之後驗證**
```shell
$ sudo apt install v4l-utils
```
</br>
**2. 安裝依賴檔 `videobuf2_vmalloc` 和 `videobuf2_v4l2`**
```shell
$ sudo modprobe -a videobuf2_vmalloc videobuf2_v4l2
```
沒有加載會出現以下錯誤:
```shell
ray@ray:~/vcam$ sudo insmod vcam.ko
insmod: ERROR: could not insert module vcam.ko: Unknown symbol in module
ray@ray:~/vcam$ sudo dmesg | tail -n 100
[10827.688772] vcam: Unknown symbol v4l2_device_unregister (err -2)
[10827.688775] vcam: Unknown symbol video_device_release (err -2)
[10827.688782] vcam: Unknown symbol video_device_release_empty (err -2)
[10827.688788] vcam: Unknown symbol vb2_ioctl_reqbufs (err -2)
[11497.071746] vcam: Unknown symbol vb2_queue_init (err -2)
...
```
</br>
**3. 掛載 `vcam.ko` 模組**
掛載後會新增三個裝置節點,`videoX` 、`vcamctl` 、`fbX`
- videoX : V4L2 device
- vcamctl : 透過操作 `vcam-util` 控制 vcam
- fbX : 控制 framebuffer
```shell
$ sudo insmod vcam.ko
```
linux kernel 的掛載方式是,呼叫 `module.c` 的 `module_init` 函式,在這裡呼叫的是 `vcam_init`
```c
static int __init vcam_init(void)
{
int i;
int ret = create_control_device(CONTROL_DEV_NAME);
if (ret)
goto failure;
for (i = 0; i < create_devices; i++)
request_vcam_device(NULL);
failure:
return ret;
}
```
`vcam_init` 內部又呼叫了 `control.c` 的 `create_control_device` 函式,此函式的作用是用 `cdev_add` 函式註冊 character device,先分配記憶體位置給 `ctldev`[註1],`ctldev->dev_class` 設置 `/dev` 目錄下要顯示的檔案名,將 `ctldev->cdev` 加入 character device,透過 `device_create` [註2]建立 `ctldev->device`
```c
int __init create_control_device(const char *dev_name)
{
int ret = 0;
ctldev = alloc_control_device;
...
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
ctldev->dev_class = class_create(dev_name);
#else
ctldev->dev_class = class_create(THIS_MODULE, dev_name);
...
cdev_init(&ctldev->cdev, &control_fops);
ctldev->cdev.owner = THIS_MODULE;
...
ret = cdev_add(&ctldev->cdev, ctldev->dev_number, 1);
...
ctldev->device = device_create(ctldev->dev_class, NULL, ctldev->dev_number,
NULL, dev_name, MINOR(ctldev->dev_number));
...
spin_lock_init(&ctldev->vcam_devices_lock);
...
}
```
註1 : `control device` 結構負責管理整個裝置,結構如下:
```c
struct control_device {
int major; // major device number,同個 module 下的 major 是一樣的,v4l2_device 就是 81
dev_t dev_number; // 第幾個裝置
struct class *dev_class; //建立 /dev 的節點
struct device *device;
struct cdev cdev;
struct vcam_device **vcam_devices; // 我們的 vcam
size_t vcam_device_count; // vcam 的裝置數量
spinlock_t vcam_devices_lock;
};
```
註2 : `device_create` 是 `linux/drivers/base/core.c` 下的函式 註解說明建立設備並且註冊到 sysfs,函式主要做的事是,分配記憶體位置給裝置,設置裝置名稱,把傳入參數設置給 dev ,最後將裝置註冊到 kernel
```c
/**
* device_create - creates a device and registers it with sysfs
...
*/
struct device *device_create(const struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, vargs);
va_end(vargs);
return dev;
}
struct device *device_create_groups_vargs(const struct class *class, struct device *parent,
dev_t devt, void *drvdata,
const struct attribute_group **groups,
const char *fmt, va_list args)
{
...
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
...
device_initialize(dev);
dev->devt = devt;
dev->class = class;
dev->parent = parent;
dev->groups = groups;
dev->release = device_create_release;
dev_set_drvdata(dev, drvdata);
retval = kobject_set_name_vargs(&dev->kobj, fmt, args);
if (retval)
goto error;
retval = device_add(dev);
if (retval)
goto error;
return dev;
...
}
```
:::danger
注意書寫規範:
* 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致
本頁面張貼的原始程式碼,應該用 4 個空白字元進行縮排。
:::
**4. 掛載成功後,會在 /dev 下新增裝置節點**
使用 `./vcam-util -l` 命令列出所有 V4L2 device
```shell
ray@ray:~/vcam$ sudo ./vcam-util -l
Available virtual V4L2 compatible devices:
1. fb1(640,480,1/1,rgb24) -> /dev/video0
```
查看 `vcam-util.c` 可以知道 -l 會呼叫 `list_devices`
```c
/* Process command line options */
do {
next_option =
getopt_long(argc, argv, short_options, long_options, NULL);
switch (next_option) {
...
case 'l':
list_devices();
break;
...
```
`list_devices` 透過 `VCAM_IOCTL_GET_DEVICE` request 取得裝置資訊,當 ioctl 不回復 0 代表裝置存在,則繼續顯示該裝置資訊,直到找不到裝置為止
```c
int list_devices()
{
struct vcam_device_spec dev = {.idx = 0};
int fd = open(ctl_path, O_RDWR);
if (fd == -1) {
fprintf(stderr, "Failed to open %s device.\n", ctl_path);
return -1;
}
printf("Available virtual V4L2 compatible devices:\n");
while (!ioctl(fd, VCAM_IOCTL_GET_DEVICE, &dev)) {
dev.idx++;
printf("%d. %s(%d,%d,%d/%d,%s) -> %s\n", dev.idx, dev.fb_node,
dev.width, dev.height, dev.cropratio.numerator,
dev.cropratio.denominator,
dev.pix_fmt == VCAM_PIXFMT_RGB24 ? "rgb24" : "yuyv",
dev.video_node);
}
close(fd);
return 0;
}
```
</br>
**5. 測試驅動程式是否正常運作**
使用 `v4l2-compliance` 進行測試,正常情況下應該要返回 Failed: 0, Warnings: 0
```shell
ray@ray:~/vcam$ sudo v4l2-compliance -d /dev/video0 -f
...
Stream using all formats:
test MMAP for Format RGB3, Frame Size 640x480@59.94 Hz:
Stride 1920, Field None: OK
test MMAP for Format RGB3, Frame Size 640x480@1.00 Hz:
Stride 1920, Field None: OK
Total for vcam device /dev/video0: 48, Succeeded: 48, Failed: 0, Warnings: 0
```
`v4l2-compliance` 會測試裝置可以支持的所有格式,透過 Memory Mapping 進行測試,Stride 1920 代表 每列有 640 個像素,每個像素有 RGB 三個資訊,共 1920 bytes,Field None: OK 代表測試結果沒問題
</br>
**6. 查看 framebuffer 資訊**
可以看到 framebuffer 儲存的是 1280x800 畫質,但我們的裝置是 640x480,目前不確定會不會發生衝突
```shell
ray@ray:~/vcam$ sudo fbset -fb /dev/fb0 --info
mode "1280x800"
geometry 1280 800 1280 800 32
timings 0 0 0 0 0 0 0
rgba 8/16,8/8,8/0,0/0
endmode
Frame buffer device information:
Name : vmwgfxdrmfb
Address : 0
Size : 4096000
Type : PACKED PIXELS
Visual : TRUECOLOR
XPanStep : 1
YPanStep : 1
YWrapStep : 0
LineLength : 5120
Accelerator : No
```
</br>
### 開啟 vcam
**1. 安裝 vlc,使用命令開啟 vcam**
```
$ sudo apt install vlc
$ vlc v4l2:///dev/video0
```
會顯示出以下畫面,代表 vcam 成功運行
![image](https://hackmd.io/_uploads/Hyeyv3KI0.png =80%x)
2. 參考 [eecheng 可開啟指定圖片](https://reurl.cc/9vk17x) 的程式碼進行測試
## TODO: 檢視其他學員在 vcam 的投入狀況,提出疑惑和建議
> 在[課程期末專題](https://hackmd.io/@sysprog/linux2024-projects)找出同樣從事 vcam 專案開發的學員,在其開發紀錄指出其他學員成果的不足和謬誤,提出你的疑惑和建議。
> 在此彙整你的認知和對比你的產出。
> 要能提出關鍵問題,例如「vcam 為何使用到 framebuffer 和 V4L2?這是基於什麼考量?」「如何消除 vcam 的記憶體洩漏的問題?」「如何降低 vcam 使用時期的 CPU 開銷?」「V4L2 內部有哪些關鍵的核心執行緒搭配處理 vcam 所需的功能?」
> 除了在其他學員的開發紀錄提問,也該嘗試從 Linux 核心原始碼找出對應解答和素材。