Cameras & sensorsHow-to

Set exposure via sensor registers when the SDK has no API

When the SDK has no exposure function, set exposure via the sensor register: find the address, compute lines from row time, write it, read back, verify.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01What you need
  2. 02Step 1: find the exposure-time register
  3. 03Step 2: compute the row time
  4. 04Step 3: compute the line count and write it
  5. 05Step 4: read back, then prove it on the image
  6. 06Common mistakes that look like a broken camera
  7. 07When the standard API is the better road
  8. 08Cameras this applies to, and the next step
  9. 09Questions

Your camera streams clean frames, but the SDK exposes no exposure function anywhere in its API, and the fixed value set at initialization no longer suits the scene. On most CMOS sensors that is not a dead end: exposure time lives in a sensor register, and an SDK that can stream almost always carries a raw sensor-register read and write too. The register does not hold microseconds. It holds a line count, and exposure time is that count multiplied by the sensor's row time. This how-to walks the register hunt, the row-time arithmetic, the write, and the verification, with numbers you can check end to end: 30 fps, a 1200-row frame, a 27.8 µs row time, and a 10 ms exposure written as 360 lines.

What you need

  • The sensor's register map, from the datasheet or from the register initialization table that ships with the camera's driver or firmware package. It gives the exposure register's address, width, and unit.
  • A register read and write pair in your SDK. The vendor's own initialization code uses exactly that pair, so it exists even on boards that never exposed an exposure feature; the function names vary by SDK.
  • The timing configuration: frame rate and total frame height, blanking rows included.
  • A uniformly lit gray card or white surface for the brightness check in Step 4.
  • Gain fixed at a known value, so a gain change cannot masquerade as an exposure change.

If the camera is a standard GigE Vision or USB3 Vision device, look for a GenICam exposure feature first; the register path is the fallback for SDKs that never exposed one.

Step 1: find the exposure-time register

Register maps print three facts per register: address, width, and unit. CMOS exposure registers hold integration lines, never time. Addresses look like 0x3012, but the exact address belongs to each sensor part, so take it from your map and from nowhere else. Width matters as much as address: a 16-bit register accepts up to 65,535, some sensors split the value across two 8-bit registers with a defined write order, and some parts bank their registers behind a page-select register.

  1. Find the exposure-time register and note its address and width.
  2. If the value splits into two 8-bit halves, note which half the datasheet says to write first.
  3. If the map uses page or bank selection, note the page register address and the value it needs.

Most exposure registers take effect from the next frame, which makes live tuning easy, while registers that change the timing itself usually want the stream stopped. Set static values before streaming, in the order set manual exposure before streaming: initialization order describes.

Step 2: compute the row time

frame period = 1 / frame rate t_row = frame period / rows per frame (total, blanking included) t_exposure = register value × t_row
  1. Compute the frame period: 1 / 30 fps = 33.33 ms.
  2. Take total rows from the timing table: 1080 active plus blanking gives 1200 here.
  3. Divide: 33.33 ms / 1200 rows = 27.8 µs per row.

If the table omits the total row count, measure the period between frame-start pulses or timestamps and divide by the rows your configuration uses: a 69.6 ms period over 1200 rows gives 69.6 ms / 1200 = 58 µs per row. The measured number is the trustworthy one, because it reflects the blanking your firmware actually chose.

Step 3: compute the line count and write it

lines = round(target exposure / t_row) actual exposure = lines × t_row

For a 10 ms target: 10,000 µs / 27.8 µs = 359.7, so the register gets 360, which the sensor delivers as 360 × 27.8 µs = 10.0 ms. Exposure granularity is one row time, 27.8 µs here. The scale works in both directions: a value of 1000 at a 20 µs row time is a 20 ms exposure.

  1. Convert the target to the register's unit: 10 ms becomes 10,000 µs.
  2. Divide by the row time: 10,000 / 27.8 = 359.7.
  3. Round to whole lines: 360.
  4. Write the value, then read it back and compare.
# Illustrative pseudocode: substitute your SDK's real register read/write calls
reg_exposure = 0x3012   # example address only: yours comes from the register map
t_row_us = 27.8         # row time from Step 2, microseconds
target_ms = 10.0        # exposure you want, milliseconds

lines = round(target_ms * 1000 / t_row_us)   # 10,000 / 27.8 = 359.7 -> 360
write_sensor_reg(reg_exposure, lines)        # your SDK's write call
readback = read_sensor_reg(reg_exposure)     # the test: must return 360

Two ceilings bound the value. The 16-bit width accepts up to 65,535, but the frame caps integration first: a rolling-shutter sensor cannot integrate longer than one frame period, which is 1200 lines ≈ 33.3 ms at this timing. Keep the value at or below the total row count, or the sensor clamps the exposure or misbehaves. Why camera exposure time has a maximum limit and how to exceed it covers the levers that raise that ceiling: a slower frame rate or a shorter frame height.

Step 4: read back, then prove it on the image

  1. Read the register back; a mismatch means a wrong bank or a half-written split value.
  2. Image the evenly lit target at 360 lines and record the mean gray level.
  3. Double the value to 720 lines: the mean gray level should roughly double while the brightest pixels stay below saturation.
  4. Confirm the frame rate still reads 30 fps.

That linearity sweep is your proof that the register you found is the register that sets exposure. If the stream carries per-frame metadata, the camera's own exposure report is the fastest cross-check; per-frame exposure and gain: verifying camera metadata shows how to read it.

A global shutter sensor spends the frame period on integration plus readout, so its ceiling is tighter. Reading out 1080 active rows costs 1080 × 27.8 µs = 30.0 ms, which leaves 33.33 − 30.0 = 3.3 ms of exposure at 30 fps. At 15 fps the period doubles to 66.7 ms and the cap rises to 66.7 − 30.0 = 36.7 ms. If the frame rate sags as you raise exposure, that arithmetic is what you are hitting; why your GigE camera frame rate drops at longer exposure times walks the mechanism.

Common mistakes that look like a broken camera

  • Writing microseconds as if they were lines. A 10,000 written into a 27.8 µs-row register is a 278 ms request (10,000 × 27.8 µs = 278 ms), clamped to one frame period, and the image settles near full-period brightness instead of the 10 ms you wanted.
  • Changing frame rate or height without recomputing the row time, so every line now carries the wrong number of microseconds.
  • Writing one half of a split 16-bit value and never the other.
  • Skipping the page-select write on a banked map, so the value lands in the wrong bank.
  • Leaving group hold latched after a timing change, so the sensor runs on its old values until you release it.
  • Running auto gain during the test, so brightness moves for two reasons at once.

When the standard API is the better road

If the camera speaks GigE Vision or USB3 Vision, a GenICam exposure feature performs this line-count arithmetic for you, and you write microseconds directly instead of register lines. When the feature exists but your code cannot see it, camera exposure property not found: check the source object sorts out the usual causes, and runtime camera parameter control: exposure and white balance live extends the same features to live tuning.

The register path stays useful on GenICam cameras as a debug tool: access GigE camera registers at runtime from your control code shows the raw read and write through the standard transport, which is how you confirm what the high-level feature wrote. If the SDK offers neither an exposure feature nor a register pair, the fallbacks in camera has no software exposure control: what to do instead — lighting, lens iris, mechanical shading — move the brightness without touching the sensor.

Cameras this applies to, and the next step

Board-level cameras are the class where this routine is day-one practice. They go into machines as bare boards or with compact lens mounts. Your code is the camera driver there, and the register pair is part of the integration from the start:

Board-level camerasHikrobot MV-CB060-10UM-B 6 MP Board Level Camera, USB3, IMX178, Mono, Bare Board, V4MV-CB060-10UM-B
Sensor format
1/1.8"
Interface
USB3
$239.00View part
Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
$690.00View part
Board-level cameras6.3 MP board-level camera, IMX178, 1/1.8" rolling shutter, 18 fps (LS-BCM-0020)LS-BCM-0020
Sensor format
1/1.8"
Lens mount
CS-mount
$290.00View part

On housed machine-vision cameras the SDK usually owns the conversion, and the register map remains your cross-check. One 12 MP, 4000 × 3000 rolling-shutter USB3 area scan model:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

Browse the area scan cameras and board-level cameras, search the camera finder, or configure a camera step by step. The camera guides collect the rest of the exposure and timing topics, and the Learn hub indexes everything else. When a timing budget needs a second opinion, contact the applications team.

Questions

Why does the exposure register hold lines instead of microseconds?

The sensor's sequencer counts rows while charge integrates, so the register stores a count of lines, not a time. The conversion lives in the timing configuration: at 30 fps with 1200 total rows the row time is 27.8 µs, so 360 lines give 10.0 ms, while at 120 fps the row time falls to 6.9 µs and the same 360 lines give 2.5 ms. Because every frame-rate or height change moves the row time, storing lines keeps the register correct across timings.

What happens if I write a line count larger than the frame height?

A 16-bit register accepts up to 65,535, but at this 1080p30 timing the frame caps integration at the 1200 total rows, about 33.3 ms. Past that cap a sensor clamps the exposure at one frame period or produces an unpredictable frame. Stay at or below the total row count. To go longer, slow the frame rate: at 15 fps the same 1200-row frame has a 66.7 ms period, and 1200 lines then hold 66.7 ms of exposure.

Does this line-count method work on global shutter sensors?

Most CMOS sensors, global and rolling shutter alike, program integration in line units; the address and width differ per part, so the register map decides. The timing budget differs too: a global shutter sensor reads out after it integrates, so exposure and readout must both fit in the frame period. At 30 fps and 27.8 µs rows, the 30.0 ms readout leaves about 3.3 ms of exposure; at 15 fps the cap rises to 36.7 ms.

Where do I find the register address if the datasheet has no register map?

Use the register initialization table that ships with the camera's driver or firmware package from the manufacturer or its distributor; it lists every address the firmware writes at power-up, including the exposure lines. If the part has neither a datasheet map nor an init table, ask the vendor's support for the exposure register definition instead of probing addresses, because one wrong write can hang the sensor until the next power cycle.

I wrote the register but the image brightness did not change. What do I check first?

Read the value back and compare; a mismatch points to a wrong bank or a half-written split value. Then confirm the page-select register held the right bank during the write, that both halves of a split value went out in the datasheet's order, and that group hold was released. Finally re-check the target: a saturated image hides a working change, so drop the light or the line count and repeat the sweep.