In the previous lecture of this free Linux kernel development course, we registered a struct v4l2_device — the top-level object that represents one physical piece of video hardware and owns every sub-device attached to it. But v4l2_device never talks to user space directly. That job belongs to a second, equally important structure: struct video_device. This lecture is a deep, field-by-field walkthrough of struct video_device, how it creates /dev/videoX nodes, and how a bridge driver registers and releases it on the current Linux kernel. As always in this free embedded Linux course, every driver example below is original and written against the latest stable V4L2 API — not copied from any book.
What You Will Learn
- Why
struct video_deviceexists and how it differs fromstruct v4l2_device - Every field of
video_deviceexplained in plain language - How
vfl_typeandvfl_dirdecide what kind of/devnode gets created - How the V4L2 core wraps your driver’s file operations before the VFS ever sees them
- How to register and cleanly unregister a video device node with
video_register_device() - A complete, original demo driver you can build and load on a real Linux machine
Prerequisites
- Lecture 1 of this chapter:
v4l2_deviceregistration basics - Comfort with character device drivers,
platform_driver, and kernel modules - A Linux machine (or VM) with kernel headers installed, and the media subsystem enabled in your
.config
Two Structures, Two Jobs: v4l2_device vs video_device
It helps to think of the V4L2 object model as a small company. struct v4l2_device is the manager — there is exactly one per physical device, and it keeps a list of every sub-device (sensor, tuner, encoder) reporting to it. struct video_device is the receptionist — it is the object that actually faces the outside world. Every time an application calls open(), ioctl(), mmap(), or read() on /dev/video0, it is video_device that answers. A single bridge driver can register several video_device instances (capture, output, VBI) that all point back to the same one v4l2_device.
Field-by-Field: struct video_device
Below is what actually matters in struct video_device on a current kernel, grouped by purpose rather than by declaration order — that grouping makes the structure far easier to remember than reading it top to bottom.
Media framework integration
- entity — wraps this video device as a node inside the media controller graph, only compiled in when
CONFIG_MEDIA_CONTROLLERis set. - intf_devnode — the media interface device node used by user-space topology tools such as
media-ctl. - pipe — the streaming pipeline this entity currently belongs to, used to prevent conflicting concurrent uses of the same hardware path.
Talking to user space
- fops — a pointer to your driver’s
struct v4l2_file_operations(open, release, read, write, poll, mmap). This is a V4L2-specific subset of the normalfile_operationstable; theinodeargument is dropped because V4L2 never needs it. - device_caps — a bitmask (
V4L2_CAP_VIDEO_CAPTURE,V4L2_CAP_STREAMING, and so on) reported back to user space throughVIDIOC_QUERYCAP. - ioctl_ops — a pointer to
struct v4l2_ioctl_ops, the table of callbacks that implement everyVIDIOC_*ioctl your device supports.
Kernel plumbing
- dev and cdev — the embedded
struct deviceand the character device object backing/dev/videoX. You never touch these by hand; the core owns them. - v4l2_dev — back-pointer to the parent
v4l2_devicethis node belongs to. Must be set before registration. - dev_parent — the physical bus device (platform/USB/PCI). If left NULL, the core copies it from
v4l2_dev->dev. - lock — an optional
struct mutex *. If you set it, the core automatically takes this lock around yourunlocked_ioctlcallback, saving you from hand-rolling that locking yourself. - release — a mandatory callback the core calls once the last reference to this
video_devicedrops. Forgetting to set it is one of the most common bugs in new drivers (see Troubleshooting below).
Device identity and streaming
- name — a fixed 32-byte device name shown to user space.
- vfl_type — which family of node to create:
VFL_TYPE_GRABBER(videoX),VFL_TYPE_VBI(vbiX),VFL_TYPE_RADIO(radioX),VFL_TYPE_SDR,VFL_TYPE_TOUCH, orVFL_TYPE_SUBDEVfor internal sub-devices with no user-facing node. - vfl_dir — direction of data flow:
VFL_DIR_RXfor capture,VFL_DIR_TXfor output, orVFL_DIR_M2Mfor memory-to-memory devices such as scalers and codecs that consume and produce buffers in the same operation. - minor, num, index — the assigned minor number, the trailing digit in
/dev/videoX, and an optional driver-assigned index used to build predictable udev rules. - queue — pointer to the
struct vb2_queuethat manages capture buffers via videobuf2. Only bridge drivers that actually move data set this; a pure sub-device leaves it NULL. - ctrl_handler — the V4L2 control handler for this node, normally inherited from
v4l2_dev->ctrl_handlerunless the driver needs a private one. - prio — tracks which open file handle currently “owns” the device for priority-sensitive ioctls like
VIDIOC_S_PRIORITY. NULL falls back to the priority state in the parentv4l2_device.
How the Core Wraps Your File Operations
A subtlety that trips up many newcomers to this free Linux device drivers course: the cdev registered for /dev/videoX does not point directly at your vdev->fops. It points at a generic, internal table owned by the V4L2 core. That internal table performs sanity checks — is the device still registered, is the requested callback actually implemented — and only then forwards the call into your driver’s fops. This is why, for example, calling mmap() on a node whose driver never implemented .mmap fails cleanly with -ENOTTY instead of crashing the kernel.
Registering and Unregistering a video_device
Once every field above is filled in, you hand the structure to the core with:
int video_register_device(struct video_device *vdev,
enum vfl_devnode_type type,
int nr);
type picks the device family (usually VFL_TYPE_GRABBER for a capture node) and nr requests a specific trailing number; pass -1 to let the core allocate the next free one. Internally this macro calls __video_register_device(), which assigns a minor number, creates the character device, and exposes it under /dev. The function assumes the structure was zero-initialized beforehand, so always allocate it with kzalloc(), never a plain stack struct.
When the driver is removed, the mirror call is:
void video_unregister_device(struct video_device *vdev);
This immediately blocks new opens on the node, but if an application still has it open, the actual memory is only freed later, when your release callback finally runs.
Original Demo: A Minimal Video Capture Skeleton
The driver below is intentionally minimal — it registers a real /dev/videoX node with correct video_device setup but skips actual DMA/streaming, which we cover later in this chapter. It is written from scratch for this free embedded systems course.
// ep_video_skel.c - minimal V4L2 video_device demo driver
#include <linux/module.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
struct ep_video_dev {
struct v4l2_device v4l2_dev;
struct video_device vdev;
};
static int ep_video_open(struct file *file)
{
pr_info("ep_video_skel: device opened\n");
return 0;
}
static int ep_video_release(struct file *file)
{
pr_info("ep_video_skel: device released\n");
return 0;
}
static const struct v4l2_file_operations ep_video_fops = {
.owner = THIS_MODULE,
.open = ep_video_open,
.release = ep_video_release,
};
static int ep_vidioc_querycap(struct file *file, void *priv,
struct v4l2_capability *cap)
{
strscpy(cap->driver, "ep_video_skel", sizeof(cap->driver));
strscpy(cap->card, "EP Video Skeleton", sizeof(cap->card));
return 0;
}
static const struct v4l2_ioctl_ops ep_ioctl_ops = {
.vidioc_querycap = ep_vidioc_querycap,
};
static void ep_video_device_release(struct video_device *vdev)
{
struct ep_video_dev *epv =
container_of(vdev, struct ep_video_dev, vdev);
v4l2_device_unregister(&epv->v4l2_dev);
kfree(epv);
}
static int ep_video_probe(struct platform_device *pdev)
{
struct ep_video_dev *epv;
int ret;
epv = kzalloc(sizeof(*epv), GFP_KERNEL);
if (!epv)
return -ENOMEM;
ret = v4l2_device_register(&pdev->dev, &epv->v4l2_dev);
if (ret) {
kfree(epv);
return ret;
}
epv->vdev.v4l2_dev = &epv->v4l2_dev;
epv->vdev.fops = &ep_video_fops;
epv->vdev.ioctl_ops = &ep_ioctl_ops;
epv->vdev.release = ep_video_device_release;
epv->vdev.vfl_type = VFL_TYPE_GRABBER;
epv->vdev.vfl_dir = VFL_DIR_RX;
strscpy(epv->vdev.name, "ep_video_skel", sizeof(epv->vdev.name));
video_set_drvdata(&epv->vdev, epv);
ret = video_register_device(&epv->vdev, VFL_TYPE_GRABBER, -1);
if (ret) {
v4l2_device_unregister(&epv->v4l2_dev);
kfree(epv);
return ret;
}
platform_set_drvdata(pdev, epv);
dev_info(&pdev->dev, "ep_video_skel registered as %s\n",
video_device_node_name(&epv->vdev));
return 0;
}
static void ep_video_remove(struct platform_device *pdev)
{
struct ep_video_dev *epv = platform_get_drvdata(pdev);
video_unregister_device(&epv->vdev);
}
static struct platform_driver ep_video_driver = {
.probe = ep_video_probe,
.remove = ep_video_remove,
.driver = {
.name = "ep_video_skel",
},
};
module_platform_driver(ep_video_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala minimal V4L2 video_device demo");
Building and Loading
# Makefile
obj-m += ep_video_skel.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
$ make
$ sudo insmod ep_video_skel.ko
Because this demo registers as a platform_driver with no matching device, bind it manually for testing, then check the result:
$ echo ep_video_skel | sudo tee /sys/bus/platform/devices/.../driver_override
$ ls /dev/video*
/dev/video0
$ v4l2-ctl --device=/dev/video0 --info
Driver Info:
Driver name : ep_video_skel
Card type : EP Video Skeleton
Bus info : platform:ep_video_skel
$ dmesg | tail -3
[ 812.221093] ep_video_skel: device opened
[ 812.221390] ep_video_skel ep_video_skel: ep_video_skel registered as video0
Unloading cleanly triggers the release chain we wired up:
$ sudo rmmod ep_video_skel
$ dmesg | tail -1
[ 902.114820] ep_video_skel: device released
vfl_type vs vfl_dir: A Quick Comparison
| Field | Purpose | Common values |
|---|---|---|
| vfl_type | Which device family / node prefix | VFL_TYPE_GRABBER, VFL_TYPE_VBI, VFL_TYPE_RADIO, VFL_TYPE_SDR, VFL_TYPE_TOUCH, VFL_TYPE_SUBDEV |
| vfl_dir | Direction of the data path | VFL_DIR_RX (capture), VFL_DIR_TX (output), VFL_DIR_M2M (memory-to-memory) |
Common Mistakes and Troubleshooting
- Missing release callback — registration fails outright, or worse, the kernel warns loudly on module unload because it cannot free a
video_devicewith no way to release its container structure. - Stack-allocated video_device — always
kzalloc()it; the registration code explicitly assumes the memory was zeroed. - Setting v4l2_dev after calling video_register_device() — the pointer must be valid before registration, since the core reads it immediately to build the device name and control handler defaults.
- Confusing vfl_type with vfl_dir — a capture-only USB camera is
VFL_TYPE_GRABBER+VFL_DIR_RX, not the other way around. - Forgetting video_unregister_device() in remove() — leaves a dangling character device that can crash user space tools still holding it open.
Best Practices
- Always zero-allocate
video_deviceand embed it inside your own driver-private structure, as shown in the demo. - Set
device_capsaccurately — user-space applications like GStreamer and ffmpeg query this field to decide which ioctls to even attempt. - Keep the
releasecallback symmetrical with your probe: undo every allocation and unregister the parentv4l2_devicethere, not inremove(). - Prefer
video_register_device()over the internal__video_register_device(); the latter is core-only.
Summary
In this lecture of our free Linux kernel development course, you learned that struct video_device is the object that actually exposes /dev/videoX to user space, sitting one level below v4l2_device in the object hierarchy. You now know what each field controls, how the V4L2 core wraps your file operations for safety, and how video_register_device() / video_unregister_device() manage the node’s lifetime. The next lecture builds on this skeleton by wiring up videobuf2 for real streaming.
FAQ
What is the difference between v4l2_device and video_device?
v4l2_device represents one physical piece of hardware and owns a list of sub-devices. video_device is the object that creates a specific /dev/videoX node and handles user-space calls for it. One v4l2_device can own several video_device nodes.
Do I need videobuf2 to register a video_device?
No. Registration only requires fops, ioctl_ops, vfl_type, vfl_dir, and a release callback. The queue field for videobuf2 is only required once you implement actual buffer streaming.
What happens if I never set the release callback?
The kernel will refuse to free the video_device cleanly and will print a warning when the last reference is dropped, since it has no way to release the container structure you embedded it in.
Can one driver register multiple video_device nodes?
Yes. This is common for devices with separate capture and output paths, each getting its own video_device that shares the same parent v4l2_device.
What does VFL_TYPE_SUBDEV mean?
It marks a video_device that represents an internal V4L2 sub-device rather than a user-facing capture or output node, typically used for exposing sub-device controls without a full /dev/videoX interface.
Why does my /dev/video node number change between boots?
If you pass -1 as nr to video_register_device(), the core assigns the next free minor number, which can shift depending on registration order. Use udev rules keyed on the name or index field for a stable symlink.
Is this content specific to any vendor SoC?
No. Everything in this free embedded Linux course is written against the mainline V4L2 core API and applies to any bridge driver, regardless of vendor.
Continue the Free Linux Kernel Development Course
Next: wiring up videobuf2 for real V4L2 streaming.
Next Lecture Course Index