V4L2 Async Matching and Binding-Free Linux Device Drivers Course

PREV_LEC | NEXT_LEC

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.

v4l2_async_notifier_operations bound complete unbind orphan sub-devices free linux device drivers 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_operations and when each fires
  • Where to register your actual /dev/videoX node relative to this lifecycle

Prerequisites

  • The previous lecture — v4l2_async_notifier and v4l2_async_connection structures and registration sequence
  • Familiarity with struct v4l2_subdev and 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_list and a populated waiting_list. Later, when the sensor probes and calls v4l2_async_register_subdev(), the core searches every registered notifier’s waiting_list for 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

v4l2_async_register_subdev(sd) called by sub-device driver
Core scans every notifier’s waiting_list for a connection that matches sd → MATCH FOUND: bind immediately, call .bound()
→ NO MATCH: add sd to the global orphan subdev_list, wait for a bridge notifier to register later

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 compares match.fwnode against the sub-device’s own fwnode.
  • If match.type == V4L2_ASYNC_MATCH_TYPE_I2C, the core compares match.i2c.adapter_id and match.i2c.address against 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);
};
CallbackFires WhenTypical Use
boundA sub-device just matched this connectionExtra setup on the sub-device via v4l2_subdev_call(); return a negative value to reject the match and unregister the sub-device
completeEvery connection in this notifier’s waiting_list has moved to done_list — only fires for the root (bridge) notifier, never for sub-device notifiersRegister the actual /dev/videoX node and the media device, since every sub-device is now guaranteed present
unbindA previously-bound sub-device is removed from the systemUnregister the video device if that sub-device was required for it to function
destroyThe framework is about to free a connection structFree 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_subdev and a pending v4l2_async_connection.
  • bound/complete/unbind/destroy are your four hooks into the lifecycle — complete is 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

PREV_LEC | NEXT_LEC

Leave a Reply

Your email address will not be published. Required fields are marked *