contributed by < hungyuhang
>
以後關於 vcam 的部份內容(例如改善項目)會更新到 期末專題頁面 上面
2020 年開發紀錄 / GitHub
vcam 測試記錄
2021 年開發紀錄 / GitHub
2022 年開發紀錄 / GitHub
2023 年開發紀錄
GitHub
Media subsystem kernel internal API –– 2. Video4Linux devices
Linux Media Infrastructure userspace API –– Part I - Video for Linux API
Media subsystem admin and user guide –– 7. Video4Linux (V4L) driver-specific documentation
The Video4Linux2 API: an introduction
Kernel subsystem documentation –– Frame Buffer
Driver implementer's API guide –– Frame Buffer Library
這些文件該怎麼讀?瀏覽了許多文件,我發現在閱讀文件時常常會遇到以下兩個問題:
以下簡介改寫自 TouchGFX 文件 - 影像緩衝區
影像緩衝是記憶體(通常是 RAM )的一部分,圖形引擎通過更新影像緩衝,將要在顯示器上顯示的下一幅圖像包含進來。
幀緩衝是 RAM 的一個連續部分,具有指定大小。
影像緩衝具有相應的寬度和高度。 因此,我們通常將影像緩衝視為記憶體的一個二維部分,可通過 x 、y 座標檢索。
影像緩衝具有相應的色彩格式。 影像緩衝中的每個條目都是該色彩格式下的色彩。 我們將影像緩衝中的每一個這樣的條目稱為像素。
CSDN - LCD Driver 筆記 - Frame Buffer
kernel.org - Kernel subsystem documentation –– Frame Buffer
CSDN - Linux framebuffer簡介及操作
維基百科 - Linux framebuffer
Linux 為 userspace 的使用者提供了一個 API ,讓 userspace 的使用者可以透過這個 API 去直接操作 frame buffer 。
以下內容節錄自 The Frame Buffer Device :
The frame buffer device provides an abstraction for the graphics hardware. It represents the frame buffer of some video hardware and allows application software to access the graphics hardware through a well-defined interface, so the software doesn’t need to know anything about the low-level (hardware register) stuff.
The device is accessed through special device nodes, usually located in the
/dev
directory, i.e./dev/fb*
.
如果要列出所有 frame buffer 裝置,可以用以下的命令:
$ ls /dev | grep fb
下列簡單的範例程式碼可以將資料 data
寫到 frame buffer fb1
裡面
#include <fcntl.h>
#include <unistd.h>
int main()
{
unsigned char data[3] = {1, 2, 3}; // dummy data
int fd = open("/dev/fb1", O_RDWR);
write(fd, data, sizeof(data));
close(fd);
return 0;
}
而對於 driver 實作者來說,frame buffer 的用法如下:
vmalloc()
分配所需的核心記憶體。以下簡介改寫自 Linux V4L2 學習
V4L2 ( Video4Linux version 2 的縮寫)是 Linux 下關於視訊擷取設備的驅動框架,為驅動程式和應用程式提供了一套統一的介面規格。
V4L2 支援的設備( device )十分廣泛,但是其中只有很少一部分本質上是真正的視訊設備:
要理解 V4L2 ,我們可以從兩個角度來看這個驅動框架
Linux Media Infrastructure userspace API –– Part I - Video for Linux API
首先這邊會從「使用者」的角度來看 V4L2 。
具體來說,這個子章節描述的,就是當 V4L2 driver 被寫好之後,應用程式端的工程師要如何去使用這個 V4L2 裝置(比如說開啟裝置或是釋放裝置),並且對 V4L2 裝置做設定以及溝通。
使用 V4L2 裝置的步驟如下:
實際上,依照實際的使用情境,有些步驟可以被忽略,並且可以不按順序執行。
Media subsystem kernel internal API –– 2. Video4Linux devices
接下來,這裡會用「 driver 編寫者」的角度來看 V4L2 。
在驅動層, V4L2 這個驅動框架已經為 driver 編寫者做了很多工作。只需要實作硬體相關的程式碼,並且註冊相關裝置即可。
硬體相關程式碼的編寫,除了編寫特定硬體的程式碼外,開頭就是將程式碼與 V4L2 框架綁定。綁定主要分為以下兩個部份:
struct
) ,與 V4L2 框架中相關連的結構體綁在一起。例如使用 v4l2_device_register()
將我們自己結構體中的 v4l2_device
子結構進行註冊。提到關係綁定,就必須介紹 V4L2 幾個重要的結構體。
struct video_device
:主要任務是負責向核心註冊字元裝置 (character device)
struct v4l2_device
:一個硬體裝置可能包含多個子裝置,例如一個電視卡除了有 capture 裝置,可能還有 VBI 或 FM tunner 裝置。而 v4l2_device
就是所有這些裝置的根節點,負責管理所有的子裝置。
struct v4l2_subdev
:子裝置,負責實現具體的功能 ( vcam 沒有用到 ) 。
v4l2_device
, v4l2_subdev
可以看作所有裝置和子裝置的基類。我們在實作自己的驅動程式時,往往需要繼承這些裝置基類,添加一些自己的 struct member 。
以下面的 struct 為例子:
struct vcam_device {
dev_t dev_number;
struct v4l2_device v4l2_dev;
struct video_device vdev;
/* input buffer */
struct vcam_in_queue in_queue;
spinlock_t in_q_slock;
spinlock_t in_fh_slock;
bool fb_isopen;
...
};
在這裡可以看到 struct vcam_device
其中一個成員就是 struct v4l2_device v4l2_dev
。所以在這邊 struct vcam_device
就是繼承了 struct v4l2_device v4l2_dev
這個基類。
接下來是綁定的範例:
struct vcam_device *vcam =
(struct vcam_device *) kzalloc(sizeof(struct vcam_device), GFP_KERNEL);
if (!vcam)
goto vcam_alloc_failure;
/* 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;
}
這裡則是透過 v4l2_device_register()
函式對 &vcam->v4l2_dev
進行綁定。
在上圖中,綠色的方框都是需要我們綁定並實作的函式。
其中 v4l2_file_operations
和 v4l2_ioctl_ops
是必須實作的。而 v4l2_subdev_ops
下的八類 ops 中, v4l2_subdev_core_ops 是必須實作的,其餘需要根據裝置類型選擇實作。比如 video capture 類裝置需要實作 v4l2_subdev_core_ops , v4l2_subdev_video_ops 。
v4l2_file_operations
:實作文件類操作,比如 open
, close
, read
, write
, mmap
等。但是 ioctl
是不需要實作的,一般都是用 video_ioctl2
代替。以下是範例:
static const struct v4l2_file_operations vcam_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
};
v4l2_ioctl_ops
: V4L2 導出給應用層使用的所有 ioctl 都是在這個地方實作的。但不必全部實作,只實作自己相關的 ioctl 即可。以下是範例:
static const struct v4l2_ioctl_ops vcam_ioctl_ops = {
.vidioc_querycap = vcam_querycap,
.vidioc_enum_input = vcam_enum_input,
.vidioc_g_input = vcam_g_input,
.vidioc_s_input = vcam_s_input,
.vidioc_enum_fmt_vid_cap = vcam_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vcam_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vcam_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vcam_s_fmt_vid_cap,
.vidioc_g_parm = vcam_g_parm,
.vidioc_s_parm = vcam_s_parm,
.vidioc_enum_frameintervals = vcam_enum_frameintervals,
.vidioc_enum_framesizes = vcam_enum_framesizes,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_expbuf = vb2_ioctl_expbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff};
v4l2_subdev_ops
:v4l2_subdev
有可能需要實作的 ops 的總和。分為 8 類,core, audio, video, vbi, tuner…等。vcam
當使用 insmod
命令來掛載 vcam
時,系統會呼叫 module.c
裡面的 vcam_init()
:
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 device 然後才建立 vcam 本身。
上面提到的 vcam_init()
會呼叫 control.c
裡面的 create_control_device()
來建立一個 control device ,這個 control device 底下的一個子裝置就是我們的主角,也就是 vcam 本身。下面是 control device 的 struct :
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;
};
以下是建立 control device 的步驟:
初始化 control device 物件
cltdev
是一個指標型態的全域變數,指向 control device 物件,下面的程式碼用 alloc_control_device()
初始化了一個 control device 物件,然後將 ctldev
指向它:
ctldev = alloc_control_device();
建立 device class
為了要建立 control device ,程式碼會先呼叫 class_create()
建立一個 device class :
ctldev->dev_class = class_create(dev_name);
在這裡,dev_name
是 class 的名稱( dev_name
在這裡的值是 "vcamctl"
)
class_create() 的功能是什麼?在這之前得先了解 class 的概念是什麼。
以下是 Linux 核心文件對 class 的描述:
A class is a higher-level view of a device that abstracts out low-level implementation details. Drivers may see a SCSI disk or an ATA disk, but, at the class level, they are all simply disks. Classes allow user space to work with devices based on what they do, rather than how they are connected or how they work.
class 就是對裝置更抽象的 point of view ,會用一個很概略的方式去看 device
而 class_create() 會建立一個 class 的 struct ,並且把指向他的指標回傳回來,稍後當我們在建立 device 的時候會用到。
建立 character device
初始化 character device
cdev_init(&ctldev->cdev, &control_fops);
ctldev->cdev.owner = THIS_MODULE;
control device 可以算是用 character device 當作基礎所作出來的一種裝置,所以使用 cdev_init() 對 control device 進行初始化。以下節錄自 cdev_init() 的文件:
Initializes cdev, remembering fops, making it ready to add to the system with cdev_add().
在上面的程式碼中,第一個參數是指向 character device 的指標,當 cdev_init
執行完畢時該指標會指向。
第二個是 file operations ,紀錄了 control device 相對應於 character device 的 callback function 。
接著來第二行就是要賦予剛才初始化的 character device 一個擁有者。
THIS_MODULE
是什麼?這個巨集代表當前的模組,型態是 struct module ,這邊的 module 就是核心模組(就是我們用 insmod
命令去掛載的那個 module ),我猜是最高 level 的模組,在這裡就是 vcamctl 。
申請 character device 的設備號
alloc_chrdev_region(&ctldev->dev_number, 0, 1, dev_name);
alloc_chrdev_region() 會向 linux kernel 申請設備號,major number 會被回傳到 dev_number
裡面。
那 major number 跟 minor number 是什麼?
執行 $ ls -l /dev
會得到以下結果:
...
crw------- 1 root root 507, 0 五 30 21:38 vcamctl
...
crw-rw---- 1 root kvm 10, 238 五 30 19:30 vhost-net
crw-rw---- 1 root kvm 10, 241 五 30 19:30 vhost-vsock
crw-rw----+ 1 root video 81, 0 五 30 19:30 video0
crw-rw----+ 1 root video 81, 1 五 30 19:30 video1
crw-rw----+ 1 root video 81, 2 五 30 19:30 video2
crw-rw----+ 1 root video 81, 3 五 30 19:30 video3
crw-rw----+ 1 root video 81, 4 五 30 21:38 video4
crw-rw-rw- 1 root root 1, 5 五 30 19:30 zero
crw------- 1 root root 10, 249 五 30 19:30 zfs
那個 507, 0
就是 major number 跟 minor number 。
O'Reilli - Major and Minor Numbers
The major number identifies the driver associated with the device.
我目前的理解:同一個 driver 驅動的 device 會用同一個 major number ,而如果很多個 device 都是用同一個 driver 驅動的話,那麼他們會有一樣的 major number ,然後有不同的 minor number 。
將 character device 加到核心裡面
ret = cdev_add(&ctldev->cdev, ctldev->dev_number, 1);
cdev_add() 把剛剛設定好的 character device 加到核心裡面,在這裡 dev_number
就是剛才新創的 device class 的號碼。
建立 control device
ctldev->device = device_create(ctldev->dev_class, NULL, ctldev->dev_number,
NULL, dev_name, MINOR(ctldev->dev_number));
節錄自 device_create() 的文件:
This function can be used by char device classes. A struct device will be created in sysfs, registered to the specified class.
上面的程式碼會將 control device 掛載,參數的部份會使用 ctldev->dev_class
跟 ctldev->dev_number
。
初始化 vcamctl 的 lock
spin_lock_init(&ctldev->vcam_devices_lock);
疑問點:cdev_add()
跟 device_create()
差在哪?
這裡是 cdev_add() 文件的敘述:
add a char device to the system
這裡則是 device_create() 文件的敘述:
creates a device and registers it with sysfs
為什麼不能只用 cdev_add() 就把裝置掛載到系統上面,還得要再另外使用 device_create() 才能把 device 成功掛載到系統上面?
針對上面的問題,這篇貼文給了一些線索,cdev_add()
比較像是在註冊,就是註冊完後這個裝置已經存在了,但是還沒被掛載到任何地方。然後需要呼叫 device_create()
才能真的把節點加到 /dev
裡面。
文章中提到說如果只用 cdev_init()
alloc_chrdev_region()
cdev_add()
的話,到時候還要再手動用 mknod 來創建設備節點 (filesystem node)。
建立完 control device 後,就可以來建立 vcam device 了。
首先程式會在 for 迴圈內呼叫 request_vcam_device()
,這個迴圈的目的是在創立多個 vcam device ,數量由 create_devices
決定:
for (i = 0; i < create_devices; i++)
request_vcam_device(NULL);
request_vcam_device()
的參數是 vcam 裝置的規格(struct vcam_device_spec
),比如說解析度,framebuffer 節點位置, video 節點位置之類的。
在 request_vcam_device()
內會先檢查一些設定是否有設定正確,如果都沒問題的話就會呼叫 create_vcam_device()
。
以下是 create_vcam_device()
所做的事情:
註冊 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);
initialize &vcam->v4l2_dev
and because the first input argument is NULL, &vcam->v4l2_dev.name
must be set before calling v4l2_device_register()
&vcam->v4l2_dev.name
is set using snprintf()
初始化 output buffer
在看程式碼之前,先大略描述一下 vcam 的緩衝區。vcam 的緩衝區分成以下兩種:
以下的程式碼就是在初始化 output buffer
ret = vcam_out_videobuf2_setup(vcam);
The videobuf2 API - LWN.net
videobuf2 是什麼?videobuf2 是 videobuf 的改版,所以在回答這個問題前我們要先知道 videobuf 是什麼
以下是深入理解linux内核v4l2框架之videobuf給的解釋:
videobuf層功能是一種在 v4l2 驅動和用戶空間當中的依附層,這話看起來有點繞,說白了就是提供一種功能框架,用來分配和管理視頻緩沖區,它相對獨立,卻又被 v4l2 驅動使用。它有一組功能函數集用來實現許多標準的POSIX系統調用,包括 read() , poll() 和 mmap() 等等,還有一組功能函數集用來實現流式(streaming)IO的 v4l2_ioctl 調用,包括緩沖區的分配,入隊和出隊以及數據流控制等操作。使用 videobuf 需要驅動程序作者遵從一些強制的設計規則,但帶來的好處是代碼量的減少和 v4l2 框架 API 的一致。
videobuf2 等於是改善 videobuf 之後的版本,但概念是一樣的
註冊 video device
snprintf(vdev->name, sizeof(vdev->name), "%s-%d", vcam_dev_name, (int) idx);
video_set_drvdata(vdev, vcam);
ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
video_set_drvdata() 的兩個參數:
vdev
: video devicevcam
: data每個 driver 都可以擁有一個 private data ,這個 private data 只能被 driver 本身所使用。因為 video_set_drvdata()
第二個參數的型別是 void *
,所以這代表你可以把把任何型別的東西(例如各種不同類型的 struct)當作這個 driver 的 private data 。
上面第二行程式碼做的就是把 vcam
設為 vdev
的 private data 。
再進一步看,其實在這裡 video_set_drvdata()
所做的事情可以被以下程式碼所取代:
vdev->dev.driver_data = vcam;
接下來來看 video_register_device()
2.4.2. Video device registration
video_register_device()
會註冊先前設定好的 video device ,然後依照第二個參數的值把相對應的節點掛載到 /dev
目錄底下。
在上面的程式碼當中,第二個引數是 VFL_TYPE_VIDEO
,所以函式就會用 /dev/videoX
這個路徑名稱來掛載 driver 。
依使用者的設定調整功能
/* Setup conversion capabilities */
vcam->conv_res_on = (bool) allow_scaling;
vcam->conv_pixfmt_on = (bool) allow_pix_conversion;
vcam->conv_crop_on = (bool) allow_cropping;
以下是 vcam 可以調整設定的東西:
allow_pix_conversion
- Allow pixel format conversion from RGB24 to YUYV. The default is OFF.allow_scaling
- Allow image scaling from 480p to 720p. The default is OFF.allow_cropping
- Allow image cropping in Four-Thirds system. The default is OFF.進一步看上面的三個選項:
allow_pix_conversion
這裡的 conversion 指的是將 RGB 跟 YUV 兩種不同色彩格式做轉換。
如果這個選項設為 false 的話,當 userspace 程式(比如說 VLC )透過 ioctl()
詢問支援的影像輸出格式時, vcam 會回應說它只支援一種格式。而 userspace 端的應用程式也無法進一步去更改 vcam 的影像輸出格式。
如果這個選項設為 true 的話,當 userspace 程式(比如說 VLC )透過 ioctl()
詢問支援的影像輸出格式時, vcam 會回應說它兩種格式都支援(透過 vcam_enum_fmt_vid_cap()
)。
這麼一來, userspace 端的應用程式就可以去設定它偏好的影像輸出格式。
接下來當 vcam 開始將影像從輸入端傳輸到輸出端時,它就會判斷輸入端跟輸出端的影像格式,如果兩邊的影像格式不同, vcam 就會做格式轉換。
參考: kevinshieh0225
的筆記
allow_scaling
allow_cropping
初始化 input buffer
ret = vcamfb_init(vcam);
創一個 input buffer , vcam 是使用 framebuffer 來實作 input buffer 的功能。
程式在這邊會呼叫 vcamfb_init()
,這個函式的 prototype 是:int vcamfb_init(struct vcam_device *dev)
下面紀錄了這個函式做了什麼事情:
為緩衝區配置核心記憶體
size = dev->input_format.sizeimage * 2;
if (!(fb_data->addr = vmalloc(size)))
return -ENOMEM;
size
是 input 圖片的大小,這個數字是用 vcam 的解析度算出來的,這裡就是要建立一個記憶體空間拿來當 framebuffer 。
那乘以二是什麼意思呢?這裡是因為 vcam 運用了 double buffering 的原理。
初始化緩衝區的資訊
fb_data->offset = dev->input_format.sizeimage;
首先,上面的程式碼將 offset
設為一個緩衝區的大小( sizeimage
),也就是第一個緩衝區跟第二個緩衝區起始位址之間的差距。
q->buffers[0].data = fb_data->addr;
q->buffers[0].filled = 0;
q->buffers[0].xbar = 0;
q->buffers[0].ybar = 0;
q->buffers[1].data = (void *) (fb_data->addr + fb_data->offset);
q->buffers[1].filled = 0;
q->buffers[1].xbar = 0;
q->buffers[1].ybar = 0;
memset(&q->dummy, 0, sizeof(struct vcam_in_buffer));
q->pending = &q->buffers[0];
q->ready = &q->buffers[1];
這裡的 q->buffers[0]
就是在紀錄第一個緩衝區的相關資訊, q->buffers[1]
則是紀錄第二個緩衝區的相關資訊。
這裡就是在初始化兩個緩衝區的資訊,比如說緩衝區他相對應的記憶體的位址在哪裡( q->buffers[x].data = ...
)。
初始化 framebuffer 相關的參數
/* set the fb_fix */
vfb_fix.smem_len = dev->input_format.sizeimage;
vfb_fix.smem_start = (unsigned long) fb_data->addr;
vfb_fix.line_length = dev->input_format.bytesperline;
/* set the fb_var */
vfb_default.xres = dev->fb_spec.width;
vfb_default.yres = dev->fb_spec.height;
vfb_default.bits_per_pixel = 24;
vfb_default.xres_virtual = dev->fb_spec.xres_virtual;
vfb_default.yres_virtual = dev->fb_spec.yres_virtual;
vcam_fb_check_var(&vfb_default, info);
下面是 vfb_fix 跟 vfb_default 的宣告:
static struct fb_fix_screeninfo vfb_fix = {
.id = "vcamfb",
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.xpanstep = 1,
.ypanstep = 1,
.ywrapstep = 1,
.accel = FB_ACCEL_NONE,
};
static struct fb_var_screeninfo vfb_default = {
.pixclock = 0,
.left_margin = 0,
.right_margin = 0,
.upper_margin = 0,
.lower_margin = 0,
.hsync_len = 0,
.vsync_len = 0,
.vmode = FB_VMODE_NONINTERLACED,
};
照官方文件描述, fb_fix_screeninfo
跟 fb_var_screeninfo
這兩個 data structure 是拿來讓 userspace 的程序跟 framebuffer 做溝通的:
struct fb_fix_screeninfo stores device independent unchangeable information about the frame buffer device and the current format. Those information can't be directly modified by applications, but can be changed by the driver
struct fb_var_screeninfo stores device independent changeable information about a frame buffer device
上面這兩個變數到時候都會被拿來設定 frame buffer
要建立一個 frame buffer 裝置的話,也會需要設定 info
:
/* set the fb_info */
info->screen_base = (char __iomem *) fb_data->addr;
info->fix = vfb_fix;
info->var = vfb_default;
info->fbops = &vcamfb_ops;
info->par = dev;
info->pseudo_palette = NULL;
info->flags = FBINFO_FLAG_DEFAULT;
info->device = &dev->vdev.dev;
INIT_LIST_HEAD(&info->modelist);
/* set the fb_cmap */
info->cmap.red = NULL;
info->cmap.green = NULL;
info->cmap.blue = NULL;
info->cmap.transp = NULL;
這個是 info
的宣告
struct fb_info *info;
這個文件簡略說明了 fb_info 的用途。
接下來是替 frame buffer 配置一個 cmap :
if (fb_alloc_cmap(&info->cmap, 256, 0)) {
pr_err("Failed to allocate cmap!");
return -ENOMEM;
}
cmap 在這邊是 Colormap 的意思, fb_alloc_cmap()
做的事情就是 allocate 一個 colormap 。
colormap 是什麼?文件裡面真的找不到他的功用,目前只能推測 colormap 是framebuffer 底下的一個東西
struct fb_cmap {
__u32 start; /* First entry */
__u32 len; /* Number of entries */
__u16 *red; /* Red values */
__u16 *green;
__u16 *blue;
__u16 *transp; /* transparency, can be NULL */
};
建立緩衝區
ret = register_framebuffer(info);
創一個 framebuffer device 出來。
當實際運行時, vcam 就會依照 q->buffers[x]
內的資訊,從 framebuffer 的記憶體位址拿東西出來。
在這裡想探討 vcam 的各項設定(比如說解析度)是怎麼被使用的,這邊就以解析度為例
static const struct v4l2_frmsize_discrete vcam_sizes[] = {
{480, 360},
{VGA_WIDTH, VGA_HEIGHT}, // {640, 480}
{HD_720_WIDTH, HD_720_HEIGHT}, // {1280, 720}
};
首先,上面的陣列所紀錄的是 vcam 支援的三種解析度
但 vcam 實際上的解析度其實也不一定會是這三種解析度的其中一種。如果把 default_vcam_spec
的長跟寬設為 100 x 100 ,它實際上的輸出尺寸就會是 100 x 100 而且也可以正常運行。
那解析度在這邊會被拿來做兩件事情:
以下是 default_vcam_spec
的宣告:
static struct vcam_device_spec default_vcam_spec = {
.width = 640,
.height = 480,
.cropratio = {.numerator = 3, .denominator = 4},
.pix_fmt = VCAM_PIXFMT_RGB24,
};
為什麼 default_vcam_spec
的長跟寬為什麼不使用 vcam_sizes
的值,而是要直接寫死在宣告裡面?
先簡略的描述這個流程
在 vcam 裡面,這兩個 buffer 都是用 FIFO queue 的概念下去實作的。
vcam_in_queue
這個 queue 是拿來儲存 framebuffer 的一些資訊(比如說實際的 buffer memory addr ),在 vcamfb_init()
會用到
vcam_in_buffer
是 vcam_in_queue
的基本組成元件,用來儲存某一個 framebuffer 的資訊。以下是 vcam_in_queue
的組成:
struct vcam_in_queue {
struct vcam_in_buffer buffers[2];
struct vcam_in_buffer dummy;
struct vcam_in_buffer *pending;
struct vcam_in_buffer *ready;
};
struct vcam_in_buffer {
void *data;
size_t filled;
size_t xbar, ybar;
uint32_t jiffies;
};
如果觀察程式碼,可以發現這些東西都沒有引用到 framebuffer 的 struct ,這裡 vcam_in_queue
跟 Linux framebuffer 之間唯一有關連的就是 framebuffer 本身的 address 。
vcam_out_queue
struct vcam_out_queue {
struct list_head active;
int frame;
/* TODO: implement more */
};
其中, active
是一個指向每個節點的 type 為 vcam_out_buffer
的鏈節串列,在 vcam 裡面是做為 FIFO queue 使用,以下是 vcam_out_buffer
的樣子:
struct vcam_out_buffer {
struct vb2_v4l2_buffer vb;
struct list_head list;
size_t filled;
};
vb
是一個緩衝區,原則上 read() system call 就會去那邊讀取資料。
簡單來說,使用端的應用程式(例如 VLC )會不停的將空的 output buffer 丟到 active
這個 FIFO queue 上面(也就是不停的做 enqueue 的動作),而 vcam 會不停的把 output buffer dequeue 下來,並且把 input buffer 的資料塞到剛才 dequeue 下來的 output buffer 裡面。
首先,在設定 videobuf2 (也就是 vcam 的 output buffer )的時候,就預先將 videobuf2 設定為下列模式:
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
這裡代表 vcam 的 videobuf2 支援 streaming 跟 read() 的存取方式 [Ref1: struct vb2_queue] , [Ref2: enum vb2_io_modes]
接下來,當應用程式(例如 vlc )來使用 vcam 的時候,它可能呼叫了某些 streaming 的 api ,然後 driver 就會開始一連串的函式呼叫:
vcam_start_streaming()
submitter_thread()
。submitter_thread()
會依照情況將 framebuffer 的資料複製到 videobuf2 的 buffer 裡面。因為 submitter_thread()
是一個裡面有無限迴圈的函式,所以只要呼叫一次它就可以不停的處理事情。
當 submitter_thread()
開始工作之後, vcam 使用者(例如 VLC)就可能會對 vcam 進行 read() 系統呼叫( system call )
當使用者執行這個系統呼叫時, Linux kernel 會去呼叫 v4l2 裝置 file operations 裡面的 .read callback function:
static const struct v4l2_file_operations vcam_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.unlocked_ioctl = video_ioctl2,
.mmap = vb2_fop_mmap,
};
vb2_fop_read() 會去讀取 vdev->queue
裡面的資料然後回傳給使用者,這邊可以把 vdev->queue
視為 vcam 的輸出緩衝區。 vdev->queue
的值是在下面程式碼被賦予的:
vdev->queue = &vcam->vb_out_vidq;
而 &vcam->vb_out_vidq
的值是在下面的函式被設定的:
int vcam_out_videobuf2_setup(struct vcam_device *dev)
{
struct vb2_queue *q = &dev->vb_out_vidq;
q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
q->drv_priv = dev;
q->buf_struct_size = sizeof(struct vcam_out_buffer);
q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
q->ops = &vcam_vb2_ops;
q->mem_ops = &vb2_vmalloc_memops;
q->min_buffers_needed = 2;
q->lock = &dev->vcam_mutex;
return vb2_queue_init(q);
}
那 vdev->queue
裡面的資料要怎麼被更新呢?
首先, Linux kernel 會透過 vb2_ops 的 buf_queue
callback function 來叫 driver 去更新 output buffer 。
在 vcam 程式裡面,buf_queue
綁定的 callback function 是 vcam_out_buffer_queue()
,這個函式會把 output buffer 的記憶體位址 enqueue 到 vcam_out_queue
的 acvite
queue 上。
然後在 submitter_thread()
裡面,程式會把那個記憶體位址從 active
上 dequeue 下來,然後把 input buffer 裡面的東西複製到那個記憶體位址上面。
在講流程前,先來看一下 struct vcam_out_queue
形別的 active
成員:
struct vcam_out_queue {
struct list_head active;
int frame;
/* TODO: implement more */
};
active
成員是一個鏈節串列,串列裡面節點的型別是 struct vcam_out_buffer
首先,使用者會呼叫 read()
系統呼叫,這會讓 vcam 的 vcam_out_buffer_queue()
被呼叫。
當 vcam_out_buffer_queue() 被呼叫時,系統會連帶的把一個 vb2_buffer 丟到函式裡面,我們接下來會做下面幾件事情:
struct vcam_out_buffer
型別的物件,並且裡面含有剛剛傳入的 vb2_buffer 。這個物件的 filled 欄位也被設為零,代表這個 buffer 是空的submitter_thread()
會把 buffer 從 active
上面「摘下」,並且把資料填入 buffer 裡面
綜合以上觀察,active
就是一個 queue ,使用端(例如 VLC) 會從 queue 的尾端做 enqueue ,然後裝置端會把節點 dequeue 出來後做處理。
好,現在可以來看 submitter_thread()
了
首先它會去看 active 裡面有沒有東西,如果沒東西的話就回去休息:
if (list_empty(&q->active)) {
pr_debug("Buffer queue is empty\n");
spin_unlock_irqrestore(&dev->out_q_slock, flags);
goto have_a_nap;
}
有的話,把 active 裡面的第一個 buffer 取出:
buf = list_entry(q->active.next, struct vcam_out_buffer, list);
list_del(&buf->list);
spin_unlock_irqrestore(&dev->out_q_slock, flags);
如果 framebuffer 沒有開啟的話,會呼叫 submit_noinput_buffer()
,這個函式會輸出「空白」的畫面(也就是預設的漸層畫面)
if (!dev->fb_isopen) {
submit_noinput_buffer(buf, dev);
反之,如果 framebuffer 有開啟的話,就會把 framebuffer 裡面的東西複製到 videobuf2 的 buffer 裡面
in_buf
會指向已經準備好的那個 input buffer ,並且透過 submit_copy_buffer()
將資料從 in_buf
複製到剛才從 active
裡面取出來的 buffer (也就是 buf
):
} else {
struct vcam_in_buffer *in_buf;
spin_lock_irqsave(&dev->in_q_slock, flags);
in_buf = in_q->ready;
if (!in_buf) {
pr_err("Ready buffer in input queue has NULL pointer\n");
goto unlock_and_continue;
}
submit_copy_buffer(buf, in_buf, dev);
unlock_and_continue:
spin_unlock_irqrestore(&dev->in_q_slock, flags);
}
module.c
功能定義傳遞到核心模組的參數,例如下面的程式碼定義了 devices_max
參數:
module_param(devices_max, ushort, 0);
MODULE_PARM_DESC(devices_max, "Maximal number of devices\n");
module.c
也實做了核心模組掛載以及卸載的入口函式( vcam_init()
跟 vcam_exit()
),並且使用 module_init
巨集來讓 Linux 核心知道當掛載以及卸載核心模組時,要呼叫哪個函式:
module_init(vcam_init);
module_exit(vcam_exit);
以上面的程式碼為例,當掛載 vcam 的時候, Linux Kernel 會呼叫 vcam_init()
,卸載 vcam 的時候, Linux Kernel 則是會呼叫 vcam_exit()
。
control.c
功能device.c
功能TODO: describe each callback function
videobuf.c
功能fb.c
功能vcam
使用 make
命令編譯核心模組,會出現以下錯誤:
$ make
make -C /lib/modules/6.5.0-35-generic/build M=/home/hungyuhang/linux2024/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/hungyuhang/linux2024/vcam/module.o
CC [M] /home/hungyuhang/linux2024/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/hungyuhang/linux2024/vcam/control.c:3:
/home/hungyuhang/linux2024/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/hungyuhang/linux2024/vcam/control.c:267:38: note: in expansion of macro ‘THIS_MODULE’
267 | 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/hungyuhang/linux2024/vcam/control.c:267:25: error: too many arguments to function ‘class_create’
267 | 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/hungyuhang/linux2024/vcam/control.o] Error 1
make[2]: *** [/usr/src/linux-headers-6.5.0-35-generic/Makefile:2039: /home/hungyuhang/linux2024/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
觀察錯誤訊息,可以發現錯誤是來自於 class_create()
呼叫。這個函式只需要一個引數,但是程式碼中卻對這個函式傳入了兩個引數。
會出現這樣的錯誤訊息,是因為 Linux Kernel API 在 Linux v6.4 之後的版本更新了對 create_class()
函式的呼叫方式。
更新前的 API 呼叫方式:
class_create(owner, name)
更新後的 API 呼叫方式:
class_create(name)
對於 Linux Kernel API 的詳細更改可以參考 commit 1aaba11 。
為了因應以上變動,需要在 vacm 專案的 control.c
內加上以下程式碼:
+ #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);
+ #endif
當核心版本大於或等於 v6.4 的時候,就會用新版 Kernel API 的方式去呼叫 create_class()
,反之則使用原來的方式去呼叫。
提交紀錄:vcam commit a7e6f0e
參考 vcam 測試記錄 - 使用 vcam 並且使用 kevinshieh0225 撰寫的測試程式碼 來做測試,下面是測試結果:
執行測試程式碼的時候需要用
sudo
來跑,不然資料會無法寫進 framebuffer
參考 eecheng
撰寫能在 video 開啟指定圖片的功能 來輸出指定圖片
原始圖片:
將圖片 resize 成 480 X 640:
將 raw data 輸出到 vcam framebuffer 的部份,我參考了 eecheng
的程式碼跟 kevinshieh0225
的程式碼重新修改了一個測試程式碼:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
static char fb_path[128] = "/dev/fb1";
static int fd;
FILE *file;
void signal_exit_handler(int sig)
{
close(fd);
fclose(file);
exit(0);
}
int main()
{
signal(SIGINT, signal_exit_handler);
fd = open(fb_path, O_RDWR);
file = fopen("sample_image/1.raw", "r");
unsigned char rgb[3];
while (1) {
file = fopen("sample_image/1.raw", "r");
for (int i = 0; i < 640; i++) {
for (int j = 0; j < 480; j++) {
memset(rgb, 0, sizeof(rgb));
fscanf(file,"%hhu %hhu %hhu", &rgb[2], &rgb[1], &rgb[0]);
write(fd, rgb, sizeof(rgb));
}
}
fclose(file);
}
return 0;
}
並且以下是輸出到 V4L2 device 的結果:
將該參數設為 1 之後,執行[輸出指定圖片]的測試,圖片的顏色會變得不一樣
:
透過 v4l2 去看媒體資訊:
可以看到編解碼器變成 YUV
解析度變成 1280x720
圖片變成這樣:
可以看到圖片的旁邊被切掉了:
並且圖片的長跟寬也變小了:
pr_debug
在 vcam 程式碼裡面使用了 pr_debug
來輸出 debug message ,但是 Linux 核心預設是把這個功能關起來的,需要手動開啟。以下是啟用的方式:
參考這篇文章
先確認 CONFIG_DYNAMIC_DEBUG 有沒有啟用
$ grep -i CONFIG_DYNAMIC_DEBUG /boot/config-6.5.0-35-generic
CONFIG_DYNAMIC_DEBUG=y
CONFIG_DYNAMIC_DEBUG_CORE=y
再來將設定寫到 debugfs 的設定檔,將你要 enable pr_debug 的原始碼(例如 device.c )的 pr_debug() 啟用。
這個步驟需要切換到 root
# echo 'file /home/hungyuhang/linux2024/vcam/device.c +p' > /sys/kernel/debug/dynamic_debug/control
最後在你要 debug 的 c file 加入這行程式碼:
#define DEBUG