V4L2 Control IDs And Callbacks-Free Linux Device Drivers Course

V4L2 Control IDs And Callbacks

Free Linux kernel development course — the V4L2_CID_* namespace, v4l2_ctrl_new_std, and the get/set/try control callback contract.

In the previous lecture of this free linux kernel development course we introduced v4l2_ctrl_handler and v4l2_ctrl and added a single brightness control to ep_camsensor. Now we go one level deeper: how v4l2 control ids are namespaced, the full v4l2_ctrl_new_std() signature, and — most importantly — the v4l2_ctrl_ops callback contract that actually moves values between userspace and your hardware. By the end of this lecture, ep_camsensor will expose several real, working controls that you can list and change with standard V4L2 tools. This continues to build the driver skill set expected in any solid free embedded linux course or free linux device drivers course.

Topics Covered
V4L2_CID_* v4l2_ctrl_handler_init v4l2_ctrl_new_std v4l2_ctrl_ops v4l2_ctrl_handler_setup

What You Will Learn

  • How v4l2 control ids are namespaced with V4L2_CID_* and organised into control classes
  • The exact v4l2_ctrl_handler_init() and v4l2_ctrl_new_std() prototypes, field by field
  • The three callbacks of struct v4l2_ctrl_opsg_volatile_ctrl, try_ctrl, s_ctrl — and when each one is needed
  • What v4l2_ctrl_handler_setup() does and why you’d call it during probe
  • A complete, original ep_camsensor example exposing brightness, contrast, saturation, and a volatile autogain-style control

Prerequisites

This lecture continues directly from the previous one on v4l2 subdevice registration and controls, and assumes your ep_camsensor driver already has a struct v4l2_ctrl_handler ctrl_handler field and a minimal v4l2_ctrl_new_std() call wired up.

The V4L2_CID_* Namespace

Every V4L2 control is identified by a unique numeric ID, always prefixed V4L2_CID_ and declared in include/uapi/linux/v4l2-controls.h. The following is a non-exhaustive set of the standard IDs you’ll encounter on almost any camera sensor driver:

#define V4L2_CID_BRIGHTNESS          (V4L2_CID_BASE+0)
#define V4L2_CID_CONTRAST            (V4L2_CID_BASE+1)
#define V4L2_CID_SATURATION          (V4L2_CID_BASE+2)
#define V4L2_CID_HUE                 (V4L2_CID_BASE+3)
#define V4L2_CID_AUTO_WHITE_BALANCE  (V4L2_CID_BASE+12)
#define V4L2_CID_DO_WHITE_BALANCE    (V4L2_CID_BASE+13)
#define V4L2_CID_RED_BALANCE         (V4L2_CID_BASE+14)
#define V4L2_CID_BLUE_BALANCE        (V4L2_CID_BASE+15)
#define V4L2_CID_GAMMA               (V4L2_CID_BASE+16)
#define V4L2_CID_EXPOSURE            (V4L2_CID_BASE+17)
#define V4L2_CID_AUTOGAIN            (V4L2_CID_BASE+18)
#define V4L2_CID_GAIN                (V4L2_CID_BASE+19)
#define V4L2_CID_HFLIP               (V4L2_CID_BASE+20)
#define V4L2_CID_VFLIP               (V4L2_CID_BASE+21)

Not every control lives under the plain V4L2_CID_BASE offset. As the controls framework grew, newer, more specialised IDs were organised under their own control-class bases instead of being crammed into the original flat list — for example:

#define V4L2_CID_VBLANK    (V4L2_CID_IMAGE_SOURCE_CLASS_BASE + 1)
#define V4L2_CID_HBLANK    (V4L2_CID_IMAGE_SOURCE_CLASS_BASE + 2)
#define V4L2_CID_LINK_FREQ (V4L2_CID_IMAGE_PROC_CLASS_BASE + 1)

V4L2_CID_VBLANK/V4L2_CID_HBLANK belong to the image-source control class (sensor-side timing, such as vertical/horizontal blanking) and V4L2_CID_LINK_FREQ belongs to the image-processing class (the pixel bus link frequency between sensor and receiver) — both far more relevant to raw camera sensor drivers than the older “user controls” list above. On the current kernel you’ll also encounter dedicated classes for JPEG compression, camera controls (focus, zoom, exposure metering), flash controls, and detection controls, each with its own class base constant.

If your hardware needs a control the standard list doesn’t cover, you’re expected to add a custom ID based on the appropriate class base and take care that it doesn’t collide with any existing ID — custom control creation itself uses v4l2_ctrl_new_custom() rather than v4l2_ctrl_new_std(), which is out of scope for this lecture but good to know exists.

Control ID namespace
V4L2_CID_BASE -> classic “user controls” (brightness, contrast, gain, flip …) V4L2_CID_IMAGE_SOURCE_CLASS_BASE -> sensor timing (VBLANK, HBLANK …) V4L2_CID_IMAGE_PROC_CLASS_BASE -> pixel pipeline (LINK_FREQ …) custom driver IDs -> built on top of the correct class base, via v4l2_ctrl_new_custom()

Initializing The Control Handler

Before any control can be created, its owning handler must be initialized with the v4l2_ctrl_handler_init() macro:

v4l2_ctrl_handler_init(hdl, nr_of_controls_hint);

hdl is the v4l2_ctrl_handler to initialize, and nr_of_controls_hint is simply an estimate of how many controls this handler will eventually own — it’s used purely to size the internal lookup hash table efficiently, not as a hard limit. When you’re finished with a handler (typically in your driver’s remove path), release it with:

v4l2_ctrl_handler_free(hdl);

v4l2_ctrl_new_std(): Creating A Standard Control

Once the handler exists, standard controls are created and attached to it with v4l2_ctrl_new_std():

struct v4l2_ctrl *v4l2_ctrl_new_std(struct v4l2_ctrl_handler *hdl,
                                     const struct v4l2_ctrl_ops *ops,
                                     u32 id, s64 min, s64 max,
                                     u64 step, s64 def);
ParameterMeaning
hdlThe control handler this new control will belong to
opsThe v4l2_ctrl_ops table providing get/set/try callbacks for this control
idThe control’s V4L2_CID_* identifier
min, maxThe accepted value range — note the core may adjust (mangle) these depending on the specific control ID’s own rules
stepThe control’s increment/decrement step
defThe control’s default value

Most of this function’s behaviour — name, type, flags — is derived automatically from the id you pass in, since standard controls already have well-known metadata baked into the core. This is exactly why the function is called “new std”: it only makes sense for the standard, predefined V4L2_CID_* IDs. For a genuinely custom control with its own name and type, use v4l2_ctrl_new_custom() instead.

struct v4l2_ctrl_ops: The Get/Set/Try Contract

A control is useless without a way to actually read or write its value against real hardware. That’s the purpose of struct v4l2_ctrl_ops:

struct v4l2_ctrl_ops {
    int (*g_volatile_ctrl)(struct v4l2_ctrl *ctrl);
    int (*try_ctrl)(struct v4l2_ctrl *ctrl);
    int (*s_ctrl)(struct v4l2_ctrl *ctrl);
};
  • s_ctrl is invoked to set the control’s value — this is the one callback nearly every writable control needs.
  • g_volatile_ctrl fetches the current value for a volatile control — one whose value can change on its own, driven by the hardware rather than by userspace, and which is typically read-only most of the time. A classic example is an auto-gain control: while auto mode is active, the actual gain value is whatever the sensor’s internal algorithm decided, not whatever was last written by a program.
  • try_ctrl, if provided, is called to validate a proposed value before it’s applied — but only worth implementing when the standard min/max/step range checks the core already performs aren’t sufficient on their own (for example, values that are only valid in certain combinations).

It’s entirely normal for one shared v4l2_ctrl_ops table to back every control your driver exposes; the callback simply switches on ctrl->id to decide which piece of hardware state to touch.

v4l2_ctrl_handler_setup(): Syncing Defaults To Hardware

After every control has been created, it’s good practice to call:

int v4l2_ctrl_handler_setup(struct v4l2_ctrl_handler *hdl);

This walks every control owned by hdl and calls its s_ctrl callback with that control’s default value. In effect, it forces your hardware registers into sync with the values the framework believes are currently active, which matters because nothing else automatically pushes the default values down to silicon the moment the handler is created.

A Complete Original Example: Controls On ep_camsensor

Let’s build out ep_camsensor‘s controls properly — brightness, contrast, and saturation as ordinary writable controls, plus an original volatile-style gain control, all sharing one ops table. None of the register names, values, or hardware behaviour below are copied from any real sensor datasheet or driver — they are illustrative placeholders for a fictional sensor.

#include <media/v4l2-ctrls.h>

#define EP_CAMSENSOR_REG_BRIGHTNESS 0x10
#define EP_CAMSENSOR_REG_CONTRAST   0x11
#define EP_CAMSENSOR_REG_SATURATION 0x12
#define EP_CAMSENSOR_REG_GAIN       0x13

static int ep_camsensor_write_reg(struct ep_camsensor *sensor, u8 reg, u8 val)
{
    /* real driver: i2c_smbus_write_byte_data(sensor->client, reg, val); */
    dev_dbg(&sensor->client->dev, "write reg 0x%02x = %d\n", reg, val);
    return 0;
}

static int ep_camsensor_read_reg(struct ep_camsensor *sensor, u8 reg, u8 *val)
{
    /* real driver: *val = i2c_smbus_read_byte_data(sensor->client, reg); */
    *val = sensor->sim_gain_value;
    return 0;
}

static int ep_camsensor_g_volatile_ctrl(struct v4l2_ctrl *ctrl)
{
    struct ep_camsensor *sensor =
        container_of(ctrl->handler, struct ep_camsensor, ctrl_handler);
    u8 val;
    int ret;

    switch (ctrl->id) {
    case V4L2_CID_AUTOGAIN:
        ret = ep_camsensor_read_reg(sensor, EP_CAMSENSOR_REG_GAIN, &val);
        if (ret)
            return ret;
        ctrl->val = val;
        return 0;
    default:
        return -EINVAL;
    }
}

static int ep_camsensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
    struct ep_camsensor *sensor =
        container_of(ctrl->handler, struct ep_camsensor, ctrl_handler);

    switch (ctrl->id) {
    case V4L2_CID_BRIGHTNESS:
        return ep_camsensor_write_reg(sensor, EP_CAMSENSOR_REG_BRIGHTNESS, ctrl->val);
    case V4L2_CID_CONTRAST:
        return ep_camsensor_write_reg(sensor, EP_CAMSENSOR_REG_CONTRAST, ctrl->val);
    case V4L2_CID_SATURATION:
        return ep_camsensor_write_reg(sensor, EP_CAMSENSOR_REG_SATURATION, ctrl->val);
    default:
        return -EINVAL;
    }
}

static const struct v4l2_ctrl_ops ep_camsensor_ctrl_ops = {
    .g_volatile_ctrl = ep_camsensor_g_volatile_ctrl,
    .s_ctrl          = ep_camsensor_s_ctrl,
};

static int ep_camsensor_init_controls(struct ep_camsensor *sensor)
{
    struct v4l2_ctrl *gain_ctrl;

    v4l2_ctrl_handler_init(&sensor->ctrl_handler, 4);

    v4l2_ctrl_new_std(&sensor->ctrl_handler, &ep_camsensor_ctrl_ops,
                       V4L2_CID_BRIGHTNESS, 0, 255, 1, 128);
    v4l2_ctrl_new_std(&sensor->ctrl_handler, &ep_camsensor_ctrl_ops,
                       V4L2_CID_CONTRAST, 0, 255, 1, 128);
    v4l2_ctrl_new_std(&sensor->ctrl_handler, &ep_camsensor_ctrl_ops,
                       V4L2_CID_SATURATION, 0, 255, 1, 128);

    gain_ctrl = v4l2_ctrl_new_std(&sensor->ctrl_handler, &ep_camsensor_ctrl_ops,
                                   V4L2_CID_AUTOGAIN, 0, 255, 1, 32);
    if (gain_ctrl)
        gain_ctrl->flags |= V4L2_CTRL_FLAG_VOLATILE;

    if (sensor->ctrl_handler.error)
        return sensor->ctrl_handler.error;

    sensor->sd.ctrl_handler = &sensor->ctrl_handler;

    return v4l2_ctrl_handler_setup(&sensor->ctrl_handler);
}

Add a u8 sim_gain_value; field to struct ep_camsensor (used only to simulate a hardware-driven volatile value for this demo), and call ep_camsensor_init_controls() right after v4l2_i2c_subdev_init() in probe(), exactly as in the previous lecture.

Build and Test

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_camsensor.ko
$ echo ep_camsensor 0x36 | sudo tee /sys/bus/i2c/devices/i2c-2/new_device

Expected dmesg output showing the handler pushing defaults to the simulated hardware during v4l2_ctrl_handler_setup():

$ dmesg | tail
[  412.001122] ep_camsensor 2-0036: write reg 0x10 = 128
[  412.001140] ep_camsensor 2-0036: write reg 0x11 = 128
[  412.001152] ep_camsensor 2-0036: write reg 0x12 = 128

If this sub-device has a registered /dev/v4l-subdevX node (from the previous lecture’s registration step), you can list and change its controls directly with the standard V4L2 control-line tool:

$ v4l2-ctl -d /dev/v4l-subdev0 --list-ctrls
                     brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=128
                       contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128
                     saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128
                       autogain 0x00980912 (int)    : min=0 max=255 step=1 default=32  value=32 flags=volatile

$ v4l2-ctl -d /dev/v4l-subdev0 --set-ctrl brightness=200

Setting brightness triggers ep_camsensor_s_ctrl(), which you’ll see reflected as another write reg 0x10 = 200 line in dmesg.

Common Mistakes and Troubleshooting

MistakeSymptomFix
Forgetting the V4L2_CTRL_FLAG_VOLATILE flag on a hardware-driven controlg_volatile_ctrl is never called; reads return a stale cached value instead of the live hardware valueSet ctrl->flags |= V4L2_CTRL_FLAG_VOLATILE right after creating that control
Not checking sensor->ctrl_handler.error after a batch of v4l2_ctrl_new_std() callsA silently failed control creation goes unnoticed until something downstream misbehavesCheck the handler’s error field once after all controls have been added, exactly as shown above
Never calling v4l2_ctrl_handler_setup()Framework believes controls are at their default values, but hardware registers were never actually writtenCall it once during probe, right after all controls are created
Implementing try_ctrl when plain min/max/step is enoughUnnecessary extra code path with no functional benefitOnly implement try_ctrl when values depend on more than a simple numeric range

Best Practices

  • Pass an accurate nr_of_controls_hint to v4l2_ctrl_handler_init() — it’s only a sizing hint, but a reasonable estimate keeps the internal hash table efficient.
  • Share one v4l2_ctrl_ops table across all standard controls and dispatch on ctrl->id, rather than writing a separate ops table per control.
  • Mark genuinely hardware-driven values (auto-gain, auto-exposure results, signal strength) as V4L2_CTRL_FLAG_VOLATILE and implement g_volatile_ctrl for them.
  • Always check ctrl_handler.error after control creation and always free the handler in your driver’s remove path.

Real-World Use Cases

Every mainline camera sensor driver under drivers/media/i2c/ follows this exact shape: a shared v4l2_ctrl_ops table, a batch of v4l2_ctrl_new_std() calls during probe, a handful of controls flagged volatile for auto-exposure/auto-gain/auto-white-balance, and a final v4l2_ctrl_handler_setup() call. Applications like guvcview, OBS Studio’s V4L2 capture source, and GStreamer’s v4l2src all talk to exactly this control interface — the brightness/contrast/gain sliders users see are backed directly by the mechanism in this lecture.

Summary / Key Takeaways

  • V4L2 control IDs are namespaced under V4L2_CID_*, with older controls sitting on the plain V4L2_CID_BASE and newer, more specialised ones organised under their own control-class bases.
  • v4l2_ctrl_handler_init() prepares a handler before any control can be added to it; v4l2_ctrl_new_std() creates and attaches a standard control, deriving most of its metadata from the ID itself.
  • v4l2_ctrl_ops has three callbacks: s_ctrl (nearly always needed), g_volatile_ctrl (only for hardware-driven values), and try_ctrl (only when range checks aren’t enough).
  • v4l2_ctrl_handler_setup() pushes every control’s default value down to hardware by calling s_ctrl for each one — call it once, after all controls are created.

Conclusion

With this lecture, ep_camsensor now exposes a small, genuinely functional set of V4L2 controls, backed by real get/set callbacks and correctly synced to simulated hardware at probe time. That completes the core building blocks of a V4L2 sub-device in this free linux kernel development course — initialization, operations, registration, and controls. From here, future lectures move into the media controller and async notifier frameworks that tie multiple sub-devices together into a full camera pipeline.

Frequently Asked Questions

What is the difference between v4l2_ctrl_new_std and v4l2_ctrl_new_custom?

v4l2_ctrl_new_std creates a control based on a standard, predefined V4L2_CID_* ID, deriving its name/type/flags automatically. v4l2_ctrl_new_custom is used for controls that don’t correspond to a standard ID and need their own explicit configuration.

When should I implement g_volatile_ctrl?

Only for controls whose value can change on its own due to hardware behaviour, such as an auto-gain or auto-exposure result. It lets the framework fetch the live value instead of returning a stale cached one.

Do I need try_ctrl for every control?

No. It’s only worth implementing when the standard min/max/step validation the core already performs isn’t sufficient to guarantee a value is actually valid for your hardware.

What does v4l2_ctrl_handler_setup actually do?

It iterates over every control owned by a handler and calls that control’s s_ctrl callback with its default value, ensuring hardware registers are actually in sync with what the framework believes is currently set.

Why are some control IDs like V4L2_CID_VBLANK not under V4L2_CID_BASE?

Newer, more specialised controls are organised under their own control-class base constants (such as the image-source or image-processing classes) instead of the original flat V4L2_CID_BASE list used by early “user controls.”

Is this part of a free course?

Yes, this is part of Embedded Pathashala’s free Linux kernel development course, which also covers Bluetooth and general embedded Linux driver topics at no cost.

Continue Your Free Linux Kernel Development Course

Next up: tying sub-devices together with the media controller and async notifier frameworks.

Next Lecture Browse Full Course Index

Leave a Reply

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