Back-to-back software triggering should pace itself at the camera's frame rate, yet the bench numbers climb 45, 60, 75, 90 ms as exposure rises in 5 ms steps — a staircase no sensor produces. You are watching two clocks at once: the sensor's exposure-plus-readout floor and the host's 15.6 ms timer tick. Work through the three causes and three tests below, read the true trigger-ready wait out of the staircase, and commit to the cycle rate the camera actually owes you. The worked example runs a 35 fps frame-rate cap and ten exposure settings from 5 to 50 ms on a Windows host.
The symptom: a wait that climbs in 15 ms steps
The loop software-triggers the camera and polls the acquisition-status signal with the frame-trigger-wait selector before each trigger. That signal reads true while the camera is ready to accept the next frame trigger — not exposing, not reading out. The reasonable expectation: with the frame rate capped at 35 fps, a 28.6 ms period, waits sit near 28.6 ms for short exposures and drop toward zero when nothing is in flight.
Ten exposure settings, one measured wait each:
| Exposure (ms) | Measured wait until ready (ms) |
|---|---|
| 5 | 45 |
| 10 | 45 |
| 15 | 45 |
| 20 | 60 |
| 25 | 60 |
| 30 | 60 |
| 35 | 75 |
| 40 | 75 |
| 45 | 75 |
| 50 | 90 |
Read the staircase before you read the camera. The wait pins at 45 ms across 5, 10 and 15 ms exposures, jumps to 60 across 20 to 30 ms, holds 75 across 35 to 45 ms, then steps to 90 at 50 ms. The step height is 15 ms and the jumps land at 15, 30 and 45 ms of exposure. Sensor timing moves smoothly with exposure; only a quantized clock steps. If your wait runs through an SDK helper with a ready-timeout, the same tick hides inside it, and the trigger-ready-timeout diagnosis carries over unchanged.
Cause 1, most likely: the host timer ticks every 15.6 ms
Windows services its default system timer at 64 Hz — one tick every 15.6 ms. A loop that calls Sleep for 1 ms asks for a millisecond, and the scheduler rounds the wake-up up to the next tick: at the default resolution the call returns near 15 ms, and near 31 ms when the loop misses a tick. Status polls then run every ~15.6 ms, see the flip up to one tick late, and the loop reports a staircase. The status read itself crosses USB or GigE in far less than a tick, so the tick dominates.
t_phase is the offset between the loop's start and the tick grid, so a reading lands anywhere from the true wait up to one tick above it. The counter that ignores ticks and schedulers entirely is QueryPerformanceCounter: it reads a high-resolution counter rather than the interrupt clock, which makes it the clock for every stamp below.
Cause 2: the stamps wrap poll, trigger and grab together
A widespread measurement pattern stamps once before the wait loop and once after the trigger call and the frame grab. Everything between lands in the number: tick quantization, trigger round trip, exposure, readout, transfer, driver buffering. That figure answers how long the whole cycle took, not when the sensor became ready. Stamp at the flip of the status signal for ready time and trigger-to-trigger for cycle time, and keep the two apart — how a capture really decomposes is its own timing lesson.
Who fires the trigger matters too: when the SDK fires it behind your loop's back, you are measuring somebody else's schedule — the ownership question. And mode matters less than you would think. Single-shot acquisition adds a host re-arm before every capture while continuous does not, yet both read the same staircase while the poll dominates.
Cause 3, and real: the floor is exposure plus readout
On a global-shutter sensor, frame N+1 cannot integrate until frame N has been read off the sensor, so exposure and readout serialize. Trigger-to-ready is exposure plus readout, and the frame-rate cap is one over the readout time. The 35 fps cap pins readout at 1/35 s = 28.6 ms. Ready-instantly-once-exposure-is-short is the wrong model: ready comes instantly only before the first trigger, when nothing is in flight.
At a 10 ms exposure the floor is 10 + 28.6 = 38.6 ms, so the best back-to-back rate is 1/38.6 ms = 25.9 fps, not 35 fps. At 50 ms it is 78.6 ms = 12.7 fps. Rising exposure eating the frame rate is this floor at work, and cutting readout with binning or ROI is the lever that raises it. The same floor explains why frame rate drops as exposure times grow.
Test each cause in order
Start with the loop, because the check costs nothing and fails loudest. This one measures what a 1 ms sleep really costs on the host:
// What does Sleep(1) actually cost on this host?
// QueryPerformanceCounter and QueryPerformanceFrequency are documented
// Windows API calls; every other name below is a local variable.
LARGE_INTEGER freq = {0}, t0 = {0}, t1 = {0};
QueryPerformanceFrequency(&freq); // counts per second
const int n = 1000; // iterations
double sum = 0.0, minMs = 1e9, maxMs = 0.0;
QueryPerformanceCounter(&t0);
for (int i = 0; i < n; i++) {
Sleep(1); // asks for one millisecond
QueryPerformanceCounter(&t1);
double ms = 1000.0 * (double)(t1.QuadPart - t0.QuadPart)
/ (double)freq.QuadPart;
sum += ms;
if (ms < minMs) minMs = ms;
if (ms > maxMs) maxMs = ms;
t0 = t1;
}
// mean = sum / n. Default-timer desktop: mean ~15.6 ms, min ~15 ms,
// outliers ~31 ms. With a 1 ms resolution request: mean ~1-2 ms.
Run it before touching the camera. A mean near 15.6 ms, minima near 15 ms and a few 31 ms outliers confirm cause 1. With a 1 ms resolution request in effect, the mean drops to about 1-2 ms, and hosts already running high-resolution timers read closer to 0.5 ms. Any mean under 3 ms clears the poll as the dominant error for cycles above 30 ms.
Second, move the stamp to the flip. Record the counter value at the moment the status turns true and again when the grab returns; on the bench pattern from cause 2, the gap between those stamps is the exposure-plus-readout span the old number folded in — 33.6 to 78.6 ms across the ten settings here.
Third, check the arithmetic. Compute each floor as exposure + 28.6 ms, divide by the 15.6 ms tick, round up, and set prediction against measurement:
| Exposure (ms) | Floor = exposure + 28.6 ms | Floor / 15.6 ms, rounded up | Predicted wait (ms) | Measured (ms) |
|---|---|---|---|---|
| 5 | 33.6 ms | 2.15 -> 3 | 46.8 | 45 |
| 10 | 38.6 ms | 2.47 -> 3 | 46.8 | 45 |
| 15 | 43.6 ms | 2.80 -> 3 | 46.8 | 45 |
| 20 | 48.6 ms | 3.12 -> 4 | 62.4 | 60 |
| 25 | 53.6 ms | 3.44 -> 4 | 62.4 | 60 |
| 30 | 58.6 ms | 3.76 -> 4 | 62.4 | 60 |
| 35 | 63.6 ms | 4.08 -> 5 | 78.0 | 75 |
| 40 | 68.6 ms | 4.40 -> 5 | 78.0 | 75 |
| 45 | 73.6 ms | 4.72 -> 5 | 78.0 | 75 |
| 50 | 78.6 ms | 5.04 -> 6 | 93.6 | 90 |
Every group lands on its predicted tick. Measured values sit 1.8 to 3.6 ms under the tick multiples because the loop starts mid-tick, exactly what t_phase predicts. The jumps fall where the floor crosses a tick — at exposures of 18.2, 33.8 and 49.4 ms (46.8 - 28.6, 62.4 - 28.6, 78.0 - 28.6) — which 5 ms settings show as steps between 15 and 20, 30 and 35, and 45 and 50 ms, precisely the observed breaks. Floor and tick are both confirmed, and the floor column is your true wait.
None of this needs exotic hardware: one software-triggerable camera reproduces the staircase on demand. A 2.5 MP color GigE camera with a global-shutter 1/1.7" sensor on a C-mount is the standard bench stand-in.
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Fix: pace with the floor, not the poll
- Let the driver pace the cycle. In continuous software-trigger mode, issue trigger and grab back-to-back and let the call block until the frame lands: one wait call per frame, no status poll in the pacing path. The loop then runs at the sensor floor by construction, and the frame-timeout diagnosis covers the case where a frame genuinely never arrives. The same discipline extends beyond live capture; the recorded-stream frame-drop guide covers the playback case.
- If the poll must stay, set the tick on purpose. Request a 1 ms resolution at startup with timeBeginPeriod and restore it on exit with timeEndPeriod. Sleep(1) then returns in about 1-2 ms; the cost is more timer interrupts on one core, a fair trade on a dedicated controller. In multi-camera rigs where blocking waits stack up, the switch from blocking to polling has its own logic — poll the frame queue, not the ready flag, and never with a default-tick sleep if you are timing anything.
- Report from device timestamps. Diff the camera's own frame timestamps for cycle time, and use the glass-to-glass latency method when the question is trigger-to-image latency.
- Go to hardware when microseconds matter. A pulse generator into the trigger input, timed against frame arrival with a counter, removes the host from the measurement entirely.
The host matters as much as the loop. A dedicated vision controller keeps the resolution request, the tick and the background tasks under your control rather than a desktop's.
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
The decision, with numbers
Commit. The 45-90 ms staircase was the poll; the camera owes the floor column. At the 35 fps cap, the back-to-back software-trigger rate is 1 / (exposure + 28.6 ms): 33.8 fps at 1 ms exposure (29.6 ms), 29.8 fps at 5 ms (33.6 ms), 25.9 fps at 10 ms (38.6 ms), 12.7 fps at 50 ms (78.6 ms). No host change moves those numbers. The levers are exposure, the frame-rate cap setting and readout — binning, ROI, or a faster-readout camera class.
When binning and ROI are spent and the cap still binds, move to a camera sold explicitly as a high-speed version; shorter readout is what that designation buys. This one is a 12 MP USB3 camera with a 1.1" global-shutter sensor on a C-mount.
- Sensor format
- 1.1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 12 MP
Browse the area scan cameras or go straight to the camera configurator once you know the floor you need. The host side lives with the vision controllers. If you want the floor checked against a cycle budget before you order, send the exposure, cap and target rate through the quote request or contact us and we will run the arithmetic with you.
Prevention: the bench checklist
More trigger and timing diagnostics sit on the cameras topic page. The list below keeps this fault off your next bench.
Before the session:
- Run the 1 ms sleep granularity loop and write the measured tick into the test record; 15.6 ms is the common default. That tick quantizes every wait number the loop will produce.
- Compute the floor per setting — exposure + 1/fps_cap — and predict the tick bins. A prediction turns the next staircase into a confirmation instead of a mystery.
During:
- Stamp with a monotonic high-resolution counter, never wall-clock time. Wall clocks step and resynchronize without warning.
- Log device timestamps with every frame. Trigger-to-trigger deltas are the cycle truth no host clock can bend.
- If a poll must run, set the resolution explicitly and restore it on exit. The request reshapes scheduling for everything the host runs.
After:
- Compare predicted tick bins against the measured groups once, then take the poll out of the pacing path. It has found the floor; it should not set the tempo.
- Re-run the granularity check after SDK, driver or OS updates. Buffering and timer behavior move without announcement.
Questions
Why does Sleep(1) in my polling loop take 15 ms instead of 1 ms?
Sleep rounds your 1 ms request up to the next servicing tick of the default system timer, about 15.6 ms apart, so the call returns after roughly 15 ms until a process raises the resolution. QueryPerformanceCounter is unaffected: it reads a high-resolution counter rather than the tick, which is why it is the right clock for trigger timing. Requesting a 1 ms resolution drops Sleep(1) to roughly 1-2 ms, at the cost of more timer interrupts.
Does SingleFrame or Continuous acquisition mode change the trigger-ready wait?
No. The mode decides how many triggers the camera accepts before re-arming, not the sensor's exposure-then-readout floor, so per-frame ready time is identical. The staircase reads the same in both modes because the poll's tick dominates. For rate tests use continuous mode: single-shot adds a host-side re-arm before every capture, which inflates the cycle with host latency that no status signal reports.
How do I measure true trigger-to-image latency instead of the poll?
Stamp with QueryPerformanceCounter immediately before the trigger call and again when the frame arrives, then subtract the exposure time you set. Better still, diff the camera's device timestamps across consecutive triggered frames: that delta is the real trigger-to-trigger period with no host clock in the path. The glass-to-glass latency method walks the full procedure, including the repeat count needed to smooth scheduler jitter.
What poll interval is fine enough for machine-vision cycle timing?
Apply the tenth rule: keep the poll period under one tenth of the shortest interval you must resolve. A 30 ms cycle therefore needs polls at 3 ms or better, reachable with a 1 ms resolution request but still the wrong tool. Once the resolution you need falls below roughly 10 ms, drop the poll, let the driver block on frame arrival, and read cycle time from device timestamps.
Does hardware triggering remove the problem entirely?
For measurement, yes. A hardware trigger enters the camera's input within microseconds of the strobe edge and the sensor paces itself, so the host, its timer and the poll loop leave the timing path. The cycle rate still equals 1 divided by exposure plus readout — the sensor floor does not move — but counter timing at the strobe edge and the frame arrival now reports the camera's true latency.


