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.
What You Will Learn
- How v4l2 control ids are namespaced with
V4L2_CID_*and organised into control classes - The exact
v4l2_ctrl_handler_init()andv4l2_ctrl_new_std()prototypes, field by field - The three callbacks of
struct v4l2_ctrl_ops—g_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_camsensorexample 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.
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);
| Parameter | Meaning |
|---|---|
hdl | The control handler this new control will belong to |
ops | The v4l2_ctrl_ops table providing get/set/try callbacks for this control |
id | The control’s V4L2_CID_* identifier |
min, max | The accepted value range — note the core may adjust (mangle) these depending on the specific control ID’s own rules |
step | The control’s increment/decrement step |
def | The 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_ctrlis invoked to set the control’s value — this is the one callback nearly every writable control needs.g_volatile_ctrlfetches 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
| Mistake | Symptom | Fix |
|---|---|---|
Forgetting the V4L2_CTRL_FLAG_VOLATILE flag on a hardware-driven control | g_volatile_ctrl is never called; reads return a stale cached value instead of the live hardware value | Set 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() calls | A silently failed control creation goes unnoticed until something downstream misbehaves | Check 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 written | Call it once during probe, right after all controls are created |
Implementing try_ctrl when plain min/max/step is enough | Unnecessary extra code path with no functional benefit | Only implement try_ctrl when values depend on more than a simple numeric range |
Best Practices
- Pass an accurate
nr_of_controls_hinttov4l2_ctrl_handler_init()— it’s only a sizing hint, but a reasonable estimate keeps the internal hash table efficient. - Share one
v4l2_ctrl_opstable across all standard controls and dispatch onctrl->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_VOLATILEand implementg_volatile_ctrlfor them. - Always check
ctrl_handler.errorafter 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 plainV4L2_CID_BASEand 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_opshas three callbacks:s_ctrl(nearly always needed),g_volatile_ctrl(only for hardware-driven values), andtry_ctrl(only when range checks aren’t enough).v4l2_ctrl_handler_setup()pushes every control’s default value down to hardware by callings_ctrlfor 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