V4L2 Async Matching and Binding
A lecture from EmbeddedPathashala’s free Linux kernel development course — what happens the moment a sub-device probes
In the previous lecture of this free linux kernel development course, we built a bridge driver that registers an async connection and waits. This lecture finishes the story: what does the async core actually do the instant a matching sub-device probes, what do the bound, complete, and unbind callbacks let you hook into, and what happens to sub-devices that probe before their bridge does? This is the piece that ties together everything V4L2 async in our free embedded systems course.
What You Will Learn
- The exact sequence of events from
v4l2_async_register_subdev()to a bound sub-device - How the async core handles sub-devices that probe before their bridge driver (“orphans”)
- Every callback in
struct v4l2_async_notifier_operationsand when each fires - Where to register your actual
/dev/videoXnode relative to this lifecycle
Prerequisites
- The previous lecture —
v4l2_async_notifierandv4l2_async_connectionstructures and registration sequence - Familiarity with
struct v4l2_subdevand basic sub-device driver probing
Two Ways In: Bridge-First or Sub-Device-First
Nothing guarantees which driver probes first — the bridge or the sensor. The async core is built to handle both orders identically from the outside:
- Bridge probes first: its notifier registers with an empty
done_listand a populatedwaiting_list. Later, when the sensor probes and callsv4l2_async_register_subdev(), the core searches every registered notifier’swaiting_listfor a connection whose match criteria fit the new sub-device. - Sub-device probes first: at the time
v4l2_async_register_subdev()runs, no notifier is waiting for it yet. The core cannot simply drop it — instead it adds the sub-device to a global list of “orphan” sub-devices, so it can be matched retroactively once a bridge does register a notifier for it.
The Matching Decision
The Matching Test Itself
A match is strictly a comparison between one struct v4l2_subdev (the real, probed sub-device) and one struct v4l2_async_connection (a description of what was expected), using the criteria in connection->match:
- If
match.type == V4L2_ASYNC_MATCH_TYPE_FWNODE, the core comparesmatch.fwnodeagainst the sub-device’s own fwnode. - If
match.type == V4L2_ASYNC_MATCH_TYPE_I2C, the core comparesmatch.i2c.adapter_idandmatch.i2c.addressagainst the I2C client backing the sub-device.
Once a match succeeds, three things happen together: the connection is removed from waiting_list and moved to done_list, the sub-device itself is registered with the V4L2 device via the core’s internal subdev registration path, and the notifier’s .bound() callback — if one is set — is invoked.
struct v4l2_async_notifier_operations — Your Three Hooks
Every hook here is optional, and every one takes a struct v4l2_async_connection * rather than the old book’s struct v4l2_async_subdev *:
struct v4l2_async_notifier_operations {
int (*bound)(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *subdev,
struct v4l2_async_connection *asc);
int (*complete)(struct v4l2_async_notifier *notifier);
void (*unbind)(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *subdev,
struct v4l2_async_connection *asc);
void (*destroy)(struct v4l2_async_connection *asc);
};
| Callback | Fires When | Typical Use |
|---|---|---|
bound | A sub-device just matched this connection | Extra setup on the sub-device via v4l2_subdev_call(); return a negative value to reject the match and unregister the sub-device |
complete | Every connection in this notifier’s waiting_list has moved to done_list — only fires for the root (bridge) notifier, never for sub-device notifiers | Register the actual /dev/videoX node and the media device, since every sub-device is now guaranteed present |
unbind | A previously-bound sub-device is removed from the system | Unregister the video device if that sub-device was required for it to function |
destroy | The framework is about to free a connection struct | Free any driver-private data attached to a custom connection wrapper struct |
complete is the one to build around: registering your /dev/videoX node from inside the bridge driver’s probe() is tempting but wrong, because at that point sub-devices may not exist yet. Waiting for complete guarantees every sub-device the notifier was waiting for has successfully bound.
Original Demo: ep_bridge Gains Its Callbacks
Extending the ep_bridge driver from the previous lecture, here is the operations struct plus the three callback implementations:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-async.h>
#include <media/v4l2-fwnode.h>
struct ep_bridge {
struct v4l2_device v4l2_dev;
struct v4l2_async_notifier notifier;
bool video_registered;
};
static int ep_bridge_bound(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *subdev,
struct v4l2_async_connection *asc)
{
struct ep_bridge *eb = container_of(notifier, struct ep_bridge, notifier);
dev_info(eb->v4l2_dev.dev, "bound sub-device: %s\n", subdev->name);
return 0;
}
static int ep_bridge_complete(struct v4l2_async_notifier *notifier)
{
struct ep_bridge *eb = container_of(notifier, struct ep_bridge, notifier);
dev_info(eb->v4l2_dev.dev, "all sub-devices bound, video pipeline ready\n");
eb->video_registered = true;
/* v4l2_device_register_subdev_nodes(&eb->v4l2_dev); goes here in a real driver */
return 0;
}
static void ep_bridge_unbind(struct v4l2_async_notifier *notifier,
struct v4l2_subdev *subdev,
struct v4l2_async_connection *asc)
{
struct ep_bridge *eb = container_of(notifier, struct ep_bridge, notifier);
dev_info(eb->v4l2_dev.dev, "sub-device %s removed\n", subdev->name);
eb->video_registered = false;
}
static const struct v4l2_async_notifier_operations ep_bridge_notifier_ops = {
.bound = ep_bridge_bound,
.complete = ep_bridge_complete,
.unbind = ep_bridge_unbind,
};
static int ep_bridge_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct ep_bridge *eb;
struct fwnode_handle *ep;
struct v4l2_async_connection *asc;
int ret;
eb = devm_kzalloc(dev, sizeof(*eb), GFP_KERNEL);
if (!eb)
return -ENOMEM;
ret = v4l2_device_register(dev, &eb->v4l2_dev);
if (ret)
return ret;
v4l2_async_nf_init(&eb->notifier, &eb->v4l2_dev);
eb->notifier.ops = &ep_bridge_notifier_ops;
ep = fwnode_graph_get_next_endpoint(dev_fwnode(dev), NULL);
if (!ep) {
ret = -ENODEV;
goto err_unregister;
}
asc = v4l2_async_nf_add_fwnode_remote(&eb->notifier, ep,
struct v4l2_async_connection);
fwnode_handle_put(ep);
if (IS_ERR(asc)) {
ret = PTR_ERR(asc);
goto err_unregister;
}
ret = v4l2_async_nf_register(&eb->notifier);
if (ret)
goto err_cleanup;
platform_set_drvdata(pdev, eb);
return 0;
err_cleanup:
v4l2_async_nf_cleanup(&eb->notifier);
err_unregister:
v4l2_device_unregister(&eb->v4l2_dev);
return ret;
}
static void ep_bridge_remove(struct platform_device *pdev)
{
struct ep_bridge *eb = platform_get_drvdata(pdev);
v4l2_async_nf_unregister(&eb->notifier);
v4l2_async_nf_cleanup(&eb->notifier);
v4l2_device_unregister(&eb->v4l2_dev);
}
static const struct of_device_id ep_bridge_of_match[] = {
{ .compatible = "ep,bridge" },
{ }
};
MODULE_DEVICE_TABLE(of, ep_bridge_of_match);
static struct platform_driver ep_bridge_driver = {
.probe = ep_bridge_probe,
.remove = ep_bridge_remove,
.driver = {
.name = "ep_bridge",
.of_match_table = ep_bridge_of_match,
},
};
module_platform_driver(ep_bridge_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("EmbeddedPathashala demo: V4L2 async bound/complete/unbind callbacks");
Build, Load, and Expected Output
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod ep_bridge.ko
sudo insmod ep_camsensor.ko # a matching sub-device driver with a fwnode endpoint
dmesg | tail -n 4
[ 15.301022] ep_bridge ep_bridge@0: bound sub-device: ep_camsensor 2-0010
[ 15.301090] ep_bridge ep_bridge@0: all sub-devices bound, video pipeline ready
[ 40.882214] ep_bridge ep_bridge@0: sub-device ep_camsensor 2-0010 removed
Notice that .complete() fires only once, right after the single expected connection binds — with multiple sub-devices, it only fires once every one of them has bound, giving you a reliable single point to finish setting up the video pipeline.
Common Mistakes and Troubleshooting
- Registering /dev/videoX from probe() instead of .complete(): this is the single most common V4L2 async bug — the video device may appear before its sub-devices are ready, or never at all if a sub-device never probes.
- Returning a positive value from .bound() expecting rejection: only a negative return value rejects a match and unregisters the sub-device; zero or positive means acceptance.
- Forgetting that .complete() never fires for sub-device notifiers: if your notifier belongs to a sub-device rather than the bridge, don’t wait on
.complete()— it only executes for the root notifier. - Not handling .unbind(): if you skip it, a hot-unplugged or rmmod’d sub-device leaves your bridge driver holding stale pointers.
Best Practices
- Always pair meaningful
.bound()/.unbind()logic — anything you set up in one should generally be torn down in the other. - Keep
.complete()focused on final pipeline assembly (video device registration, media device registration) rather than per-sub-device configuration, which belongs in.bound(). - Log at all three callback points during bring-up — it is the fastest way to see whether your device tree graph and match criteria are actually correct.
Summary and Key Takeaways
- Sub-devices and bridges can probe in either order — the async core reconciles this with a global orphan list for sub-devices that arrive first.
- A match is a strict fwnode or I2C comparison between a real
v4l2_subdevand a pendingv4l2_async_connection. bound/complete/unbind/destroyare your four hooks into the lifecycle —completeis where a bridge driver should finish assembling its video pipeline.
That completes the V4L2 async and graph-binding picture in this free linux kernel development course: from why async exists, through the fwnode graph and media bus parsing, to connection registration and the full matching lifecycle. From here, the natural next stop is the Linux Media Controller framework — entities, pads, and links — which ties everything you’ve learned into a complete capture pipeline.
Frequently Asked Questions
What happens if a sub-device probes before its bridge driver?
The async core adds it to a global list of orphan sub-devices. When a bridge driver later registers a notifier with a matching connection, the core retroactively matches against that orphan list.
Does .complete() ever fire more than once?
No — it fires exactly once, when the notifier’s waiting_list becomes empty, and only for the root (bridge) notifier, never for a sub-device notifier.
Where should I register my /dev/videoX device node?
Inside the .complete() callback, since that is the only point guaranteed to run after every expected sub-device has successfully bound.
How do I reject a sub-device match inside .bound()?
Return a negative error code from .bound() — the async core will then unregister the sub-device instead of completing the match.
What is the difference between unbind and destroy?
unbind fires when a bound sub-device is removed from the system and is where you tear down anything set up in bound(); destroy fires when the connection struct itself is about to be freed, for cleaning up driver-private data attached to it.
Is this part of a free course?
Yes — this lecture completes the V4L2 async section of EmbeddedPathashala’s free linux kernel development course.
Continue the Free Linux Kernel Development Course
Next: the Linux Media Controller framework — entities, pads, and links.
Browse All Lectures Join the Free Course