# 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 核心原始碼找出對應解答和素材。