The camera is detected, frames stream, autofocus works — and v4l2-ctl --list-ctrls prints nothing for every /dev/video node you try. That combination is usually not a broken driver: it is two driver stacks answering different questions. The camera framework sees the sensor and drives it; the capture node you queried never owned the controls. This guide is for engineers bringing board cameras up on embedded Linux. It shows you how to enumerate every node the camera registered, find the subdev that holds exposure, gain and focus, set them from your application, and undo the legacy-overlay experiment that hides the camera from both stacks. The commands are the standard Linux camera tools, so the same sequence works wherever a native sensor driver runs under libcamera.
Two stacks, two answers
Modern embedded OS images ship camera support in two layers. The kernel layer holds the sensor driver, the CSI-2 receiver and the capture nodes. On top sits a user-space camera framework — libcamera on most current boards — which loads a pipeline handler, applies ISP tuning and runs automatic exposure, white balance and autofocus.
Each layer answers to different tools. The framework's test and capture tools talk to the framework and see the full control set. V4L2 tools such as v4l2-ctl talk to kernel nodes directly. On a system that drives its pipeline through the media controller, the capture node publishes formats and buffers, while the sensor's exposure and gain controls are published on the sensor's V4L2 subdev node — the framework consumes them there.
So an empty --list-ctrls on /dev/video0 is a diagnosis, not a fault: the node you queried is a capture node on a media-controller system. On an old-style webcam stack with no media controller, every sensor control does appear under /dev/video0, which is what legacy code assumes. When your application was written for that model, the fix is to query the right node or the framework's control API — not to replace the driver.
First test: list every node, not just /dev/video0
Run the census before anything else:
v4l2-ctl --list-devices
A single embedded camera typically registers four kinds of node: a capture video node for the image, a second video node for the sensor's embedded metadata, a media node that holds the topology, and one or more V4L2 subdev nodes — /dev/v4l-subdev0 and upward — for the sensor, the ISP blocks and the lens actuator.
Query each subdev in turn:
v4l2-ctl -d /dev/v4l-subdev0 --list-ctrls
Exposure and analogue gain answer on the sensor's subdev. Focus on a motorised lens answers on the actuator's own subdev, so keep querying until controls appear. If --list-devices itself fails with a permission error, sort node access out first — Linux camera device permissions shows the checks in order.
Map the pipeline once, then stop guessing
The media controller prints the whole graph, so you can read which subdev belongs to the sensor instead of probing blind:
media-ctl -p -d /dev/media0
The entity named after the sensor chip is the subdev that carries its controls; follow the links down to the capture node. Formats advertised on a video node can be cropped or scaled versions of the sensor's native array, so do not read sensor modes off the capture node — sensor resolution vs video resolution scaling shows what that difference does to your numbers.
The framework side has its own listing. The libcamera test tool cam prints the controls an application may set; -c selects the camera:
cam --list-controls
Between the two listings you know which layer owns exposure, gain and focus, and which node answers for each.
Worked example: 4656 × 3496 detected, two empty video nodes
Inputs, from a bring-up log of exactly this shape:
- The framework detection tool lists camera 0: a 4656 × 3496 sensor in 10-bit RGGB, five modes — 1280 × 720 at 80.01 fps, 1920 × 1080 at 60.05 fps, 2328 × 1748 at 30.00 fps, 3840 × 2160 at 18.00 fps, 4656 × 3496 at 9.00 fps.
-
v4l2-ctl -d /dev/video0 --list-ctrlsreturns nothing, and so does/dev/video1: two capture nodes queried, zero controls returned. - The dim-scene inspection needs a 40 ms exposure — the one illustrative input here; everything else is measured.
The rule: capture nodes carry formats, the sensor subdev carries the controls, and every mode bounds exposure, because the frame period is the longest exposure that cannot clip a frame:
Arithmetic: 1000 / 80.01 fps = 12.5 ms; 1000 / 60.05 fps = 16.7 ms; 1000 / 30.00 fps = 33.3 ms; 1000 / 18.00 fps = 55.6 ms; 1000 / 9.00 fps = 111.1 ms. A 40 ms exposure is longer than 33.3 ms, so the 80.01, 60.05 and 30.00 fps modes are out. It is shorter than 55.6 ms, so the 18.00 and 9.00 fps modes fit.
Result: 2 capture nodes × 0 controls = 0 — an empty answer about the wrong layer, with the real control set sitting on one sensor subdev (exposure, gain) and one actuator subdev (focus). Decision: keep the native stack, take exposure and gain from the sensor subdev or the framework, take focus from the actuator subdev, and run the inspection at 18.00 fps, which holds the 40 ms exposure with a 15.6 ms margin (55.6 - 40 = 15.6). A legacy V4L2 overlay would not move those numbers; it would remove the camera from detection entirely — the subject of the next section.
The overlay experiment that hides the camera
When the empty list frustrates, the tempting fix is the one many threads suggest: install a legacy V4L2 driver package for the camera and add its dtoverlay= line to /boot/firmware/config.txt. On an OS that already ships the sensor's native driver, this is the one change that makes things strictly worse.
The legacy module and the native driver both probe the same sensor over the same I2C bus. Whichever binds first wins, the loser never registers, and the framework — which needs the native driver — sees no camera at all. Detection that listed five modes now lists none. The legacy path rarely publishes the controls you wanted on the capture node either, so you lose the camera and gain nothing.
Revert in this order:
- Delete the
dtoverlay=line you added to/boot/firmware/config.txt. Leave the OS's own camera lines untouched. - Reboot and rerun the detection tool; the sensor and its mode list should return.
- If the camera stays missing, read the kernel log with
dmesg: a failed probe or a sensor still claimed by the wrong module shows up there. A camera that loads the wrong sensor driver walks that read step by step.
Prevention: install camera drivers only from the OS image or the camera vendor's documented package for that sensor, and record which driver bound at bring-up.
Set exposure and focus through the supported path
Three routes, in the order to try them.
Framework controls. cam --list-controls shows exposure time, analogue gain and the autofocus controls the framework publishes. Your application sets them through the framework's own API, which arbitrates them against automatic exposure — no node hunting, no driver edits.
OpenCV through a compatibility layer. Where the OS provides a V4L2 compatibility path, cv2.VideoCapture('/dev/video0') streams frames, but exposure and gain pushed through V4L2 can lose to the framework's automatic exposure. That is a different fault from an empty list: the controls exist but do not win. V4L2 exposure and gain settings ignored by a sensor driver separates the two cases, runtime camera exposure control without reloading the driver changes exposure live, and camera has no software exposure control lists workarounds when nothing is published at all.
GenICam on an industrial camera. Machine-vision cameras expose exposure and gain as camera features, set by name from your code — setting exposure time through a GenICam API in Python shows the shape of that call.
Autofocus either way: focus on a near target, then a far target, and confirm both frames are sharp before trusting the loop. The two-shot check takes seconds and catches a stalled actuator.
When your code must own the node
If your plant software opens /dev/video* and sets V4L2 controls directly, the clean answer is a camera designed as one self-contained pipeline: an industrial USB3 or GigE board camera. Sensor, control logic and frame transport travel with the camera, exposure and gain are camera features set through the camera's SDK, and there is no host-side subdev to find. The same ownership logic is why area scan cameras on GigE or USB3 never show this fault.
New embedded builds get the same argument: a module that ships its own documented driver keeps control ownership with the camera, and your host code stays at the SDK level. Start from board level camera selection for embedded vision machines and the board camera evaluation checklist for inspection systems, then narrow with the camera finder.
Two modules cover the common cases. For motion work, the LS-BCM-0003 pairs a 1/1.8" global-shutter sensor with USB3 and a C-mount at 3.1 MP and 120 fps:
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 5 MP
For a GigE drop at higher resolution, the LS-BCM-0020 runs a 6.3 MP IMX178 rolling-shutter sensor at 18 fps onto a CS-mount:
- Sensor format
- 1/1.8"
- Lens mount
- CS-mount
- Resolution
- 6-10 MP
- Shutter
- Rolling
Compact cells with tight optics often take the M12-lensed MV-CB016-10GM-M-W, a 1.6 MP global-shutter module on GigE with a 1/2.9" sensor.
Checks before you touch the config
- Framework detection first. If the tool that streams frames also lists the sensor and its modes, hardware and driver are alive and the control hunt is a software-layer question.
- Full node census before any query.
v4l2-ctl --list-devices— one node's silence says nothing about the camera. - Topology before queries. Print the media graph and read which subdev the sensor entity maps to, so the first
--list-ctrlslands on the right node. - Every subdev gets one query. Exposure and gain answer on the sensor subdev, focus on the actuator subdev; a no-controls answer from one subdev is not an answer about the camera.
- One driver per sensor. At bring-up, read
dmesgand record which module claimed the sensor; a second camera driver package loaded later is how both stacks break. - Multi-camera hosts: rerun the census after reboots, because device paths can swap. Multiple USB cameras on Linux shows how to pin them down.
- Pin the validated kernel and driver versions in your deployment image. A silent package upgrade can swap the driver out from under a working node.
More bring-up fixes live on the systems topic page.
The decision, case by case
- Camera detected, frames streaming,
--list-ctrlsempty on the video nodes: wrong layer, not a fault. Query the sensor subdev for exposure and gain, the actuator subdev for focus, or set them through the framework's control API. - Empty list after installing a legacy V4L2 package: remove the package the way its vendor documents, delete the added
dtoverlay=line, reboot, confirm detection is back, then use the supported control path above. - Camera gone from detection after an overlay edit: same revert, then the kernel-log read from the section above.
- Plant code that must control by node: pick a self-contained industrial board camera and drive exposure and gain through its SDK.
If you are selecting rather than fixing, the board-level cameras card below leads to the range and its filters:
Compact cameras to build in
120 models · from $179.00Build the camera side of the station step by step:
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
When a stack resists after these checks, contact the applications team with your node census and kernel log — the fastest route to a fix.
Questions
Why does the framework camera tool see my camera but v4l2-ctl shows no controls?
The framework tool talks to the camera framework, which loads the sensor's native driver, the ISP pipeline and the autofocus logic. v4l2-ctl reads kernel nodes directly. On a media-controller stack the capture node publishes formats and buffers only, so an empty control list is the expected answer there; the sensor's own controls wait on its subdev node. Query the subdev, or list the framework's controls with the cam tool.
Where are the focus controls on a motorised lens?
On a separate node. The lens actuator, the small motor that moves the focus element, registers as its own V4L2 subdev, distinct from the image sensor's subdev. Run --list-ctrls on every subdev: exposure and analogue gain answer on the sensor's node, focus position answers on the actuator's node. Frameworks merge them under one autofocus control set, which is why autofocus works while the raw nodes look empty.
Can I still use OpenCV and cv2.VideoCapture on a libcamera camera?
Usually yes for frames, but not for sensor control. Where the OS provides a V4L2 compatibility path, VideoCapture opens a capture node and streams; the sensor's exposure and gain still belong to the framework, so calls that set them can be ignored while automatic exposure runs. Either drive exposure through the framework's control API, or move to an industrial module whose SDK owns exposure and gain.
How do I recover the camera after adding a legacy camera overlay?
Remove the overlay line you added to the boot configuration file, leave the OS's own camera lines in place, and reboot. Detection should list the sensor and its modes again. If it stays absent, read the kernel log for probe errors: a conflicting driver still claiming the sensor shows up there. Never reinstall a second driver package on top; one sensor is driven by one driver, and the native one already matched.
Do industrial GigE and USB3 board cameras have this empty-controls problem?
No. An industrial camera is one self-contained pipeline: the sensor, its control logic and the frame transport travel with the camera, not inside the host's media graph. You set exposure, gain and other features by name through the camera's SDK or the GenICam feature list, and frame capture goes over the standard interface. You never hunt for the subdev that owns the sensor, because the camera itself owns it.

