You ported a sensor driver from one embedded platform to another — in the case behind this guide, a Jetson Orin NX under JetPack 5.1.1, coming from a Jetson Nano under JetPack 4.6.1. The module enumerates on I2C (dmesg shows test_imx485 9-001a), frames flow through V4L2 and GStreamer, and every gain and exposure request is ignored: gain 1 or 16, 450 µs or 4.5 ms, the driver prints the same lines each time. This guide shows where the request dies, the five-minute v4l2-ctl test that proves it, the unit conversions the driver owes you, and the fix.
Two different requests, identical dmesg
The capture side did its job. nvarguscamerasrc ran with awblock=true aelock=true, gainrange='16 16', exposuretimerange='4500000 4500000' and ispdigitalgainrange='1 1', and the Argus daemon logged the ranges back: Setting Gain Range : 16 16 and Setting Exposure Time Range : 4500000 4500000. awblock=true waits for the auto algorithms to settle and aelock=true freezes them, so with a pinned range every frame should carry your value. The request reached the capture stack.
The driver then printed, in both runs: try to set gain to 9, initial gain 9 to imx485 register, IMX485_MAX_GAIN 300, IMX485_MIN_GAIN 0, GAIN_FACTOR 10, setting gain 3 to imx485 register, Framelength 3006, initial exposure time: 130447, setting coarse time: 669, Stream ON Success. Identical output for different inputs is the diagnosis: these writes are start-up defaults hard-coded in the stream-on path, and your request never reaches them.
One more clue sits in the mode list Argus prints from the driver's own mode table: 3840 x 2160 at 33 fps and 1920 x 1080 at 60 fps, analog gain 1.000000–31.622776, exposure 450000–400000000 ns. The pipeline negotiated 25 fps, but the 4K mode only offers 33 fps, so the sensor runs at 33 — use 33 in the timing math below. The mode table advertises ranges the control path never learned to deliver.
Where the request dies: the control handler
V4L2 routes a control set call through the driver's control handler: the framework looks the control up there, checks its range, and calls the driver's set callback, which writes registers over I2C. If the driver never registers gain and exposure in that handler, the call fails as an unknown control and the capture stack carries on with defaults — no fatal error, no log line you would notice. A port from rel-32 to rel-35 loses exactly this piece, because the platform's camera driver layer was rewritten between those releases and a driver that streams looks finished.
Every number in the stream-on printout is therefore the driver talking to itself. Gain 9 goes in, register 3 comes out, and neither traces back to the 1 or the 16 you asked for. The exposure default 130447 becomes coarse time 669 through a conversion you will check in a moment.
The five-minute proof with v4l2-ctl
-
v4l2-ctl --list-devices— find the sensor subdevice node, of the form/dev/v4l-subdev3. -
v4l2-ctl -d /dev/v4l-subdev3 --list-ctrls— a wired driver lists gain and exposure with minimum, maximum, step and current value. - Missing rows, or a handler error, mean the controls were never registered: go straight to the fix below.
- If the rows exist, set the gain to 160 while streaming, read the value back, and watch dmesg. A register print carrying your number means the driver is fine and the fault sits above it.
Setting controls on a live stream is the same discipline as changing camera gain at runtime without stopping the pipeline on interface cameras. A capture API that offers only on and off for a property is a different fault with its own diagnosis path.
Units: multipliers, register codes and coarse time
The driver has to translate between what you request and what the sensor's registers take.
At 3840 x 2160 and 33 fps a frame lasts 1/33 s = 30303030 ns, and the framelength is 3006 lines, so one line is 30303030 / 3006 ≈ 10080 ns. Exposure requests travel in nanoseconds; the sensor integrates in lines.
| You request | The control carries | The sensor needs |
|---|---|---|
| Gain 16x | 16.0 multiplier | 16 × 10 = 160, inside 0–300 |
| Gain 31.622776x | 31.622776 multiplier | 316, above the 300 ceiling |
| Exposure 450 µs | 450000 ns | 450000 / 10080 ≈ 45 lines |
| Exposure 4.5 ms | 4500000 ns | 4500000 / 10080 ≈ 446 lines |
| Exposure 400 ms | 400000000 ns | 39682 lines, thirteen frames |
Two consistency checks fall out of the driver's own prints. First, the gain ceiling: 300 register units at GAIN_FACTOR 10 is 30x, but the mode table advertises 31.622776x, so requests between the two pass the range check and then clamp. Second, the exposure default: 130447 with coarse time 669 implies 130447 / 669 ≈ 195 ns per line, but the active mode's line time is 10080 ns — a factor of 52 apart. One of the driver's constants is stale from the old platform's mode table.
Worked example: gain 16 and 4.5 ms
Take the second run's request and push it through the conversions the driver should implement.
- Gain: 16 × 10 = 160 register units, inside IMX485_MIN_GAIN 0 and IMX485_MAX_GAIN 300, so no clamping.
- Exposure: 4500000 ns / 10080 ns per line = 446 lines. Back-check: 446 × 10080 ns = 4.49 ms.
- Budget: 446 lines against a 3006-line frame leaves 2560 lines of readout, so the frame rate holds at 33 fps.
- The minimum request: 450000 / 10080 = 44.6, rounded to 45 lines = 453.6 µs — which is why the mode table quotes 450000 ns as the floor.
- The advertised 400 ms ceiling: 400000000 / 10080 = 39682 lines. That only works if the driver extends the framelength and lets the frame rate fall; otherwise cap requests just under 3006 lines ≈ 30.3 ms.
After the fix, dmesg shows your numbers: a gain print carrying 160 and a coarse time of 446 for this request, changing when the request changes.
The fix, in the driver
- Give the subdevice a control handler and register the two controls in it with the mode table's ranges: gain 1.000000–31.622776 as a multiplier, exposure 450000–400000000 ns.
- Point their set callbacks at the register-write functions that already exist — the ones printing
setting gain N to imx485 registerandsetting coarse time: N. - Convert units inside the callback: multiplier × 10 for gain; nanoseconds / line time for coarse time, clamped below the framelength.
- Attach the handler to the subdevice so the framework owns routing; one handler per subdevice is the standard shape.
- Fix the stale constants while you are in there: the line-time constant behind 195 ns per line, and the 30x versus 31.622776x gain ceiling.
- Rebuild, reload, and repeat the v4l2-ctl test before you touch GStreamer again.
On interface cameras the same request is a single call — setting camera exposure time through a GenICam API in Python shows the whole exchange. Once your controls answer, per-frame exposure and gain: verifying camera metadata shows how to prove the values that actually exposed each frame.
When the usual answer is wrong
- The controls exist and v4l2-ctl moves them, but the Argus output still ignores you: you are setting a node the stack does not use. Sensor controls live on the subdevice node, not on the video capture node.
- The stack exposes sensitivity instead of raw gain: some pipelines route analog gain through
V4L2_CID_ISO_SENSITIVITYwith a few discrete steps. Set that one and watch dmesg before writing more code. - The module runs its own auto exposure: modules with built-in firmware AE rewrite the registers every frame. Disable it in the module's bring-up sequence, then retest.
- dmesg changes but the image does not: exposure takes effect at the next frame start. Capture several frames and judge the last one, not the first.
- The
bwmgr API not supportedspam looks guilty and is not. It comes from the platform's bandwidth manager and appears in healthy captures too; keep it out of the fault search.
Checks before you call it fixed
-
--list-ctrlsshows both controls with min, max and step taken from the mode table. - Gain 160 and exposure 446 print in dmesg for this request, and different requests print different numbers.
- The driver's line time equals 10080 ns at 3840 x 2160, 33 fps, framelength 3006 — and nothing prints 195 ns per line any more.
- The advertised gain maximum equals the register ceiling divided by 10.
- An exposure above one frame either extends the framelength or is refused; it never silently clamps.
- Two capture runs with different ranges produce two different dmesg sequences — the test that started this.
Sidestep: cameras whose controls need no port
If the port has already cost more days than the module did, the alternative is a camera with a standard machine-vision interface. USB3 Vision and GigE Vision cameras describe exposure and gain as GenICam features over the cable; the host stack sets them directly and there is no sensor driver to port. For embedded machines, the compact footprints live in board-level cameras, and the trade-offs are covered in board level camera selection for embedded vision machines; full-size sensors start in area scan cameras.
Two USB3 modules that take that route:
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
- Sensor format
- 1/1.7"
- Interface
- USB3
The LS-BCM-0003 is a 3.1 MP board-level camera with a global-shutter sensor at 120 fps. The LS-CMR-0166 is a 12 MP USB3 area scan camera at 30 fps. Both answer exposure and gain through the standard feature tree, so the debugging above stays on your bench, not in your production code.
Once exposure answers, industrial camera image quality settings: exposure, gain, balance covers choosing good values. Browse the camera finder for the full interface list, start the system configurator at the camera step, or bring your dmesg lines to the contact page and we will work the numbers with you.
Questions
Why does dmesg print 'try to set gain to 9' whatever I request?
That print sits in the stream-on routine, which writes start-up defaults before the first frame. It runs once per stream-on and never reads a control, so gain 1 and gain 16 both produce gain 9 and a register write of 3. Once the control handler is wired, that routine should read the control's current value instead of a hard-coded 9, or be deleted so the control callback is the only path to the registers.
How do I convert exposure in nanoseconds to the sensor's coarse time?
Divide by the line time, which the units section below works out for the 4K, 33 fps mode. A 4.5 ms request is 4500000 / 10080 = 446 lines. Clamp the result below the framelength: exposure longer than one frame (about 30.3 ms in that mode) needs a longer framelength and a lower frame rate, or it must be refused.
Why does the mode table advertise 31.6x gain but the driver clamp at 30x?
The mode table tells the capture stack the analog gain range 1.000000 to 31.622776, while the register path uses IMX485_MIN_GAIN 0, IMX485_MAX_GAIN 300 and GAIN_FACTOR 10, so 300 / 10 = 30x is the real ceiling. Requests between 30x and 31.622776x pass the range check and then clamp. Align the two numbers: raise the register ceiling or lower the advertised maximum to 30x.
Are the 'bwmgr API not supported' messages related to this fault?
No. They come from the platform's bandwidth manager complaining about an API revision, and they appear in healthy captures too. They have no connection to the control path: gain and exposure are register writes on the sensor subdevice, not bandwidth requests. Ignore them while you debug controls, and keep them out of your search terms so they do not lead you down the wrong path.
Can I avoid writing a sensor driver at all?
Yes — see the 'Sidestep' section at the end of this guide for cameras with a standard machine-vision interface. On those, the host stack sets exposure and gain directly over the cable and there is no sensor driver to port. Board-level modules with USB3 or GigE fit the same embedded enclosures. If the port has already cost more days than the module did, switching to a standard interface is usually the faster route to a stable inspection station.

