If you have been following this free Linux kernel development course, you already know how a bridge video device turns raw camera pixels into a /dev/videoX stream using the videobuf2 framework. But a bridge device rarely works alone. Modern SoCs stack multiple hardware IP blocks — a camera sensor, an image signal processor, a scaler, a CSI receiver — one after another before a frame is ready. This lecture explains the v4l2 sub-device abstraction that the V4L2 core uses to model this chain, and walks through a complete, original v4l2 sub-device driver example built against the latest kernel APIs. This is exactly the kind of practical, from-scratch coverage you’ll find throughout our free embedded Linux course.
What You Will Learn
- Why the sub-device concept exists and how it models real camera hardware pipelines
- The complete, current
struct v4l2_subdevlayout — field by field, on the latest kernel - How sub-device flags and device nodes differ from a normal bridge
/dev/videoXnode - How to initialize, register, and clean up a sub-device the modern way
- A full original I2C sub-device driver you can build and load on a real board or QEMU
Prerequisites
- Completion of the earlier lectures in this V4L2 series (v4l2_device, video_device, videobuf2)
- Comfort with basic I2C client driver structure in the Linux kernel
- A configured kernel build environment (see our free Linux device drivers course setup lecture)
Why V4L2 Needs the Sub-Device Concept
Picture a camera pipeline on a typical embedded SoC: a sensor sitting on an I2C bus captures raw Bayer data, hands it to a CSI-2 receiver, which feeds an image signal processor that does demosaicing, color correction, and scaling, before the final pixels land in memory through DMA. None of these stages, on their own, is “the camera.” Each is a distinct piece of silicon with its own registers, its own power sequencing, and its own set of controls — exposure on the sensor, gamma on the ISP, cropping on the scaler.
Early V4L2 drivers tried to cram all of this into one monolithic bridge driver, which meant every new sensor or ISP variant required rewriting the whole driver. The v4l2 sub-device model solves this by giving every non-DMA stage in the pipeline its own independent driver object. Each stage becomes a sub-device, and the final DMA-capable stage — the one that actually owns the /dev/videoX node — is the bridge. The bridge driver talks to each sub-device through a well-defined operations table instead of poking sensor registers directly.
This modular split gives you two big wins. First, reuse — the same sensor driver can sit behind a dozen different bridge chips without changes, because it only ever talks the sub-device operations language, never bridge-specific register offsets. Second, composability — the Media Controller framework can expose this whole chain as a graph of entities and pads that userspace tools like media-ctl can inspect and rewire, which is essential once a single SoC supports several camera pipelines at once.
Bridge Nodes vs Sub-Device Nodes
Every V4L2 element — bridge or sub-device — is built on the same underlying video device machinery, but they expose different device node patterns to userspace:
| Aspect | Bridge Device | Sub-Device |
|---|---|---|
| Device node pattern | /dev/videoX | /dev/v4l-subdevX |
| Owns DMA? | Usually yes | No |
| Created automatically? | Yes, on video_register_device() | Only if V4L2_SUBDEV_FL_HAS_DEVNODE is set |
| Talks to userspace apps? | Directly, via ioctl/read/mmap | Rarely; mostly driven by the bridge or media-ctl |
A sub-device only gets a /dev/v4l-subdevX node if its driver explicitly asks for one, because most sub-devices are perfectly happy being controlled internally by the bridge and never need direct userspace access.
struct v4l2_subdev on the Latest Kernel
The core abstraction for any v4l2 sub-device driver is struct v4l2_subdev, declared in <media/v4l2-subdev.h>. The layout has evolved noticeably since older reference books were written — several fields were reworked when the subdev active-state and locking rework landed, so treat any struct listing you find in an old PDF or blog post as a starting point only, not ground truth. Here is the field set as it stands on current mainline:
struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)
struct media_entity entity;
#endif
struct list_head list;
struct module *owner;
bool owner_v4l2_dev;
u32 flags;
struct v4l2_device *v4l2_dev;
const struct v4l2_subdev_ops *ops;
const struct v4l2_subdev_internal_ops *internal_ops;
struct v4l2_ctrl_handler *ctrl_handler;
char name[V4L2_SUBDEV_NAME_SIZE];
u32 grp_id;
void *dev_priv;
void *host_priv;
struct video_device *devnode;
struct device *dev;
struct fwnode_handle *fwnode;
struct list_head async_list;
struct list_head asc_list;
struct v4l2_subdev_platform_data *pdata;
struct mutex *state_lock;
struct led_classdev *privacy_led;
struct v4l2_subdev_state *active_state;
u64 enabled_pads;
bool s_stream_enabled;
};
A few things worth calling out against what older material teaches:
- No more standalone
of_node. Older kernels kept a separatestruct device_node *of_nodepointer for device-tree lookups. Current kernels rely purely onfwnode, which transparently wraps either a device-tree node or an ACPI node, so your driver code never needs to branch on firmware type. internal_opsis now a first-class field for open/close/registered/unregistered callbacks used mainly by the sub-device’s own file handle (/dev/v4l-subdevX), separate from the driver-facingopstable.active_statereplaces the old per-open “try format” pattern. Instead of every subdev driver hand-rolling its own pad configuration storage,v4l2_subdev_init_finalize()allocates astruct v4l2_subdev_statethat tracks the sub-device’s active format, crop, and compose rectangles per pad, with built-in locking throughstate_lock.asc_listsupersedes the oldasd/notifier/subdev_notifiertrio from the original v4l2-async design for most modern async matching, though some of those older fields may still appear depending on kernel version — always check the header for the version you are targeting.
The flags field still carries the same intent as before, even though a few bit values have been added over the years:
#define V4L2_SUBDEV_FL_IS_I2C (1U << 0) /* sub-device is an I2C device */
#define V4L2_SUBDEV_FL_IS_SPI (1U << 1) /* sub-device is an SPI device */
#define V4L2_SUBDEV_FL_HAS_DEVNODE (1U << 2) /* create /dev/v4l-subdevX */
#define V4L2_SUBDEV_FL_HAS_EVENTS (1U << 3) /* sub-device can raise events */
#define V4L2_SUBDEV_FL_STREAMS (1U << 4) /* sub-device supports streams */
Initializing a Sub-Device the Modern Way
The recommended pattern is to embed struct v4l2_subdev inside your own device-specific state structure, never allocate it standalone. This lets the core find your private fields through simple pointer arithmetic, and it keeps the sub-device’s lifetime tied to the lifetime of your device state:
struct ep_camsensor {
struct v4l2_subdev sd;
struct i2c_client *client;
struct v4l2_ctrl_handler ctrl_handler;
struct media_pad pad;
struct mutex lock;
};
Initialization on a current kernel follows four steps: fill in the subdev core fields with v4l2_subdev_init(), attach your operations table, finalize active-state tracking with v4l2_subdev_init_finalize(), and only then register it with the parent v4l2_device or the async subsystem:
v4l2_i2c_subdev_init(&sensor->sd, client, &ep_camsensor_ops);
sensor->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
ret = v4l2_subdev_init_finalize(&sensor->sd);
if (ret)
return ret;
sensor->pad.flags = MEDIA_PAD_FL_SOURCE;
ret = media_entity_pads_init(&sensor->sd.entity, 1, &sensor->pad);
if (ret)
goto err_finalize;
v4l2_subdev_init_finalize() is the piece most older material simply does not mention, because it did not exist yet — it allocates and wires up the active-state object that the rest of the pad-format and crop/compose helpers expect. Skipping it is a common source of NULL-pointer crashes when a driver later calls something like v4l2_subdev_get_fmt().
Original Demo Driver: ep_camsensor
To make this concrete, here is a small original I2C sub-device skeleton, ep_camsensor, built from scratch for this lecture. It only implements enough to prove registration, pad initialization, and device-node creation — real sensor register programming is left out intentionally so the V4L2 plumbing stays visible.
#include <linux/i2c.h>
#include <linux/module.h>
#include <media/v4l2-subdev.h>
#include <media/v4l2-device.h>
struct ep_camsensor {
struct v4l2_subdev sd;
struct media_pad pad;
};
static const struct v4l2_subdev_ops ep_camsensor_ops = {
/* pad/video ops would go here in a real sensor driver */
};
static int ep_camsensor_probe(struct i2c_client *client)
{
struct ep_camsensor *sensor;
int ret;
sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
v4l2_i2c_subdev_init(&sensor->sd, client, &ep_camsensor_ops);
sensor->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
ret = v4l2_subdev_init_finalize(&sensor->sd);
if (ret)
return ret;
sensor->pad.flags = MEDIA_PAD_FL_SOURCE;
ret = media_entity_pads_init(&sensor->sd.entity, 1, &sensor->pad);
if (ret) {
v4l2_subdev_cleanup(&sensor->sd);
return ret;
}
dev_info(&client->dev, "ep_camsensor: sub-device registered as %s\n",
sensor->sd.name);
return 0;
}
static void ep_camsensor_remove(struct i2c_client *client)
{
struct v4l2_subdev *sd = i2c_get_clientdata(client);
v4l2_device_unregister_subdev(sd);
media_entity_cleanup(&sd->entity);
v4l2_subdev_cleanup(sd);
dev_info(&client->dev, "ep_camsensor: sub-device removed\n");
}
static const struct i2c_device_id ep_camsensor_id[] = {
{ "ep_camsensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ep_camsensor_id);
static struct i2c_driver ep_camsensor_driver = {
.driver = { .name = "ep_camsensor" },
.probe = ep_camsensor_probe,
.remove = ep_camsensor_remove,
.id_table = ep_camsensor_id,
};
module_i2c_driver(ep_camsensor_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala demo v4l2 sub-device driver");
Building and Loading the Driver
Assuming this file is saved as ep_camsensor.c inside a minimal out-of-tree module directory with a matching Makefile:
$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_camsensor.ko
$ echo "ep_camsensor 0x10" | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
Expected dmesg Output
$ dmesg | tail -5
[ 102.334112] ep_camsensor: loading module
[ 102.335900] ep_camsensor 1-0010: ep_camsensor: sub-device registered as ep_camsensor 1-0010
[ 102.336014] media: entity 'ep_camsensor 1-0010' registered with 1 pad
Verifying the Sub-Device Node
$ ls /dev/v4l-subdev*
/dev/v4l-subdev0
$ media-ctl -p
- entity 1: ep_camsensor 1-0010 (1 pad, 0 link)
type V4L2 subdev subtype Sensor flags 0
pad0: Source
Seeing the /dev/v4l-subdev0 node appear, and the entity show up in media-ctl -p output, confirms the sub-device registered correctly with both the V4L2 core and the Media Controller graph.
Common Mistakes and Troubleshooting
- Forgetting
v4l2_subdev_init_finalize(). Any helper that touchesactive_state— including many pad-format helpers — will crash or return-EINVALwithout it. - Setting
V4L2_SUBDEV_FL_HAS_DEVNODEunnecessarily. Most sensor and ISP sub-devices never need a direct userspace node; only set this flag if you genuinely need/dev/v4l-subdevXaccess. - Leaking the media entity on remove. Always pair
media_entity_pads_init()withmedia_entity_cleanup(), and pairv4l2_subdev_init_finalize()withv4l2_subdev_cleanup()in your remove path. - Assuming
of_nodestill exists. Code copied from older sub-device drivers that dereferencessd->of_nodedirectly will fail to build on current kernels — usefwnodeinstead.
Best Practices
- Always embed
struct v4l2_subdevin a private, device-specific structure — never allocate it alone. - Use
v4l2_i2c_subdev_init()for I2C sub-devices instead of hand-filling the name and owner fields. - Keep sub-device operations tables minimal at first — implement only the ops your pipeline actually calls, and grow from there.
- Mirror your probe-time setup order exactly in reverse during remove to avoid use-after-free on the media entity or ctrl handler.
Summary and Key Takeaways
The v4l2 sub-device abstraction exists so that camera pipelines built from many independent hardware blocks can be modeled cleanly, with each block owning its own driver and exposing a standard operations interface to whichever bridge sits above it. On current kernels, struct v4l2_subdev has grown active-state tracking, dedicated locking, and a cleaner firmware-node model compared to what older textbooks describe, and v4l2_subdev_init_finalize() is now a mandatory step in any correct sub-device driver. With the ep_camsensor example in this lecture, you have seen the complete registration path from probe to a visible /dev/v4l-subdevX node and a Media Controller entity — the same pattern every real-world sensor driver in the kernel tree follows.
Continue the Free Linux Kernel Development Course
Next up: integrating this sub-device with the V4L2 async notifier and Media Controller link validation.
Browse All Lectures Join the Free Linux Device Drivers CourseFrequently Asked Questions
What is a v4l2 sub-device in the Linux kernel?
A v4l2 sub-device is a driver object representing one non-DMA stage in a camera or video pipeline — such as a sensor, CSI receiver, or ISP — modeled with struct v4l2_subdev and controlled by a bridge driver through a standard operations table.
How is a sub-device different from a bridge video device?
A bridge device owns the DMA path and exposes /dev/videoX directly to applications, while a sub-device usually has no DMA capability and only gets its own /dev/v4l-subdevX node if the driver explicitly sets V4L2_SUBDEV_FL_HAS_DEVNODE.
Do I always need to create a /dev/v4l-subdevX node?
No. Most sub-devices are controlled internally by the bridge driver and never need a device node. Only set the has-devnode flag if userspace tools need to talk to the sub-device directly.
What does v4l2_subdev_init_finalize() actually do?
It allocates and attaches the sub-device’s active-state object, which tracks per-pad format, crop, and compose state with built-in locking, replacing the ad-hoc “try format” storage older drivers implemented by hand.
Why was the of_node field removed from struct v4l2_subdev?
Current kernels standardized on fwnode, which works for both device-tree and ACPI firmware, so a separate device-tree-only pointer became redundant and was dropped from the struct.
Can a sub-device driver be reused across different bridge chips?
Yes — that is the main benefit of the sub-device model. As long as the sub-device implements the operations the bridge expects, the same sensor or ISP driver can sit behind many different bridge implementations unchanged.
Is this course a good starting point for a free embedded systems course on cameras?
Yes. This lecture is part of our ongoing free linux kernel development course, and earlier lectures in the same series cover the video_device, file operations, and videobuf2 foundations this sub-device material builds on.
