Code readingTroubleshooting

V4L2 DQBUF stalls after a few frames: buffer starvation math

Why VIDIOC_DQBUF hangs after as many frames as you queued buffers, the runway math that proves starvation, and the loop fixes that restore the frame rate.

LumaSpec Vision11 min read Share on LinkedIn

On this page 9 sections
  1. 01What the stall count tells you
  2. 02Two queues decide whether DQBUF returns
  3. 03Runway math: five queued buffers at 50 fps with a 0.96 MB write in the loop
  4. 04Why the 100 ms sleep hides the stall
  5. 05Fix the loop: five changes that restore full rate
  6. 06When it is not starvation: signal-chain checks
  7. 07Prevention checklist
  8. 08When the usual fixes are wrong
  9. 09Questions

Your capture program returns a handful of frames and then hangs forever inside VIDIOC_DQBUF. In O_NONBLOCK mode the same loop gets -1 and EAGAIN on every call. That signature is rarely a sensor-timing fault: the stall lands after roughly as many frames as you queued buffers, which means the driver ran out of empty buffers to fill. This page gives you the queue arithmetic that proves it, a worked example at 50 fps with a disk write inside the capture loop, the five loop changes that restore the full frame rate, and the tests that separate starvation from a format, lane-mapping or driver fault when you are bringing up a custom MIPI CSI-2 sensor.

What the stall count tells you

Blocking mode delivers frames until DQBUF blocks forever; O_NONBLOCK mode delivers the same frames, then EAGAIN on every call. Count what you got before the stall. When the count moves with the buffer count — queue 10 instead of 5 and the stall moves from frame 5 to frame 10 — the capture queue ran dry. The sensor, the pixel clock and the MIPI lane rate are innocent until that test says otherwise.

Check what you actually queued, not what you requested. One recent bring-up called VIDIOC_REQBUFS with count = 25 but its mmap-and-QBUF loop covered only indexes 0-4, so 5 buffers entered the queue. The program stalled after about 5 frames, matching the queued count. VIDIOC_REQBUFS only reserves memory; a buffer joins the incoming queue when you QBUF its index, so the 20 never-queued buffers protected nothing.

If the stall count does not move with the buffer count — the first DQBUF never returns, or frames come out corrupted from frame 1 — go straight to the signal-chain checks near the end. That is a format, endpoint or driver problem, and no amount of queue tuning will fix it.

Two queues decide whether DQBUF returns

A V4L2 driver keeps two queues: empty buffers you submit with VIDIOC_QBUF wait in the incoming queue to be filled by the hardware, and completed frames wait in the outgoing queue until your code dequeues them with VIDIOC_DQBUF. By default DQBUF blocks while the outgoing queue is empty; with O_NONBLOCK it returns -1 with EAGAIN instead. The V4L2 API names these two calls precisely because the split between reserving a buffer (REQBUFS), queueing it (QBUF) and dequeuing it (DQBUF) is what your loop has to keep balanced.

The driver can only fill what sits in the incoming queue. At 50 fps a frame completes every 20 ms, so the loop must hand a buffer back at least that often. Hold buffers longer and the incoming queue empties; the receiver then has nowhere to write the next frame and drops it. Many embedded MIPI capture pipelines go one step further and stop completing buffers altogether once starved: blocking DQBUF then waits forever, non-blocking DQBUF answers EAGAIN on every call, and only a STREAMOFF-then-STREAMON cycle re-arms the channel. Pipelines that keep running instead show silent frame drops — either way, the loop is the cause.

The failure pattern is generic producer-consumer starvation, the same shape as when a motion controller's trajectory buffer empties and the axes jerk; the buffer-starvation write-up from motion control covers the identical mechanics from the servo side. In both cases the fix is consumption rate and queue depth, not the producer's settings.

Runway math: five queued buffers at 50 fps with a 0.96 MB write in the loop

The numbers below are the case behind this article: an 800 × 600 UYVY sensor at 50 fps, 25 buffers requested, 5 queued, and a full-frame fwrite between DQBUF and QBUF.

  • Frame format: UYVY, 2 bytes per pixel, 800 × 600 pixels — set with VIDIOC_S_FMT and confirmed with VIDIOC_G_FMT.
  • Frame rate: 50 fps, so one frame every 20 ms.
  • Queue depth: 25 buffers requested with VIDIOC_REQBUFS, 5 actually queued with VIDIOC_QBUF.
  • Per-iteration work: fopen, one fwrite of the whole frame, fclose, all between DQBUF and QBUF.

Rule in words: a loop keeps the full frame rate only if every cycle — DQBUF, work, QBUF — finishes within one frame period. The queue does not remove a slow cycle; it only delays the failure. Runway is buffers divided by frame rate, and in-loop work must sustain frame size times frame rate.

  • Frame size: 800 × 600 px × 2 B/px = 960,000 B ≈ 0.96 MB per frame.
  • Write rate to keep 50 fps in the loop: 960,000 B × 50 = 48,000,000 B/s = 48 MB/s, or 384 Mbit/s.
  • Runway before the incoming queue is empty: 5 buffers ÷ 50 fps = 0.1 s; with all 25 requested buffers queued, 25 ÷ 50 = 0.5 s.
T_cycle ≤ 1 / fps keep the full frame rate t_empty = N / fps runway once re-queueing stops N ≥ T_worst × fps + 2 buffers to bridge the worst loop cycle

Result: five buffers hold 0.1 s of frames. Any cycle longer than 20 ms spends that backlog, and if the storage cannot sustain 48 MB/s the queue is dry 0.1 s after STREAMON and frames stop completing.

Decision: with the fwrite inside the loop, the hang is a question of when, not whether, unless the write path sustains 48 MB/s with margin. Take the write off the loop and size the queue for the worst cycle: at these numbers a 60 ms worst case needs 0.06 × 50 = 3, plus 2, so 5 buffers.

Why the 100 ms sleep hides the stall

A usleep(100000) after DQBUF stretches the cycle to roughly 110 ms, so the loop consumes about 9 fps. The five frames completed in the first 100 ms never drain out of the outgoing queue, so DQBUF always finds a completed frame waiting and capture looks continuous. The sensor still delivers 50 fps, and frames land only as fast as buffers come back — roughly 41 of every 50 are dropped, with no error reaching your code.

Skipping QBUF when non-blocking DQBUF returns EAGAIN moves the wrong way: every skipped re-queue permanently removes one buffer from circulation. With 5 buffers, five skips in a row leave the pipeline nothing to fill, which is why the EAGAIN storm grows worse after a few seconds.

Calling STREAMON on every iteration re-arms a channel that starvation halted, which is why it appears to help. The intended state machine is STREAMON once, DQBUF/QBUF cycling, STREAMOFF to stop or recover. If you do restart, restart cleanly — the node that still holds the camera after a restart is the next trap in that path.

Fix the loop: five changes that restore full rate

  1. Take the write out of the capture loop. The capture thread does exactly DQBUF → dispatch → QBUF; disk writes, encoding, drawing and logging belong to a worker. If a GUI shares the thread, split them — running fetch off the main thread is the same fix at the application layer, and acquisition faster than processing covers matching the two rates.
  2. Re-queue every buffer, every time, including frames flagged V4L2_BUF_FLAG_ERROR — count errors, do not hoard the buffers. Never touch mmap memory after QBUF; the driver owns it from that call onward. If the worker copies, copy before you re-queue: copy the buffer before you queue it shows the tearing you get otherwise.
  3. Size the queue from the worst loop time, not from a copied example: N ≥ T_worst × fps + 2. At 50 fps with a 60 ms worst case that is 0.06 × 50 = 3, plus 2, so 5 buffers. Buffers bridge bursts only; the average cycle still has to beat the 20 ms period for the full 50 fps.
  4. Bound every wait. Open with O_NONBLOCK and poll() with a timeout before each DQBUF, or wrap blocking DQBUF in select() with a timeout. A starved channel then surfaces as a logged timeout you can act on instead of an infinite hang. Remember that a dequeued buffer in your hands is runway being spent: at 50 fps, one buffer is 20 ms.
  5. Recover deliberately when a stall happens anyway: STREAMOFF, QBUF all N buffers, STREAMON, and log the lost-frame count. If stale or duplicate frames show up after the restart, that is the leftover outgoing queue — draining the buffer queue covers it.

Minimum buffer counts from the formula, for illustration:

Frame rate Frame period Worst loop cycle N = T_worst × fps + 2
50 fps 20 ms 15 ms 3
50 fps 20 ms 60 ms 5
50 fps 20 ms 100 ms 7
120 fps 8.3 ms 20 ms 5
120 fps 8.3 ms 50 ms 8

At the 0.96 MB frames of this case, 8 buffers occupy about 7.7 MB of memory — a small footprint on any capture platform. At 12 MP UYVY, 4000 × 3000 px × 2 B = 24 MB per frame, the same 8 buffers occupy about 192 MB, so size the queue from real frame sizes, not from habits.

When it is not starvation: signal-chain checks

If the stall stays at frame 1 whatever you queue, or a plain v4l2-ctl capture cannot pull a single frame, the queue is not the suspect. Work down this list:

  • Format round-trip: set UYVY with VIDIOC_S_FMT, read it back with VIDIOC_G_FMT. A driver that silently substituted another pixel format will discard or misread every buffer.
  • YUV payload support: many platform capture drivers validate sensor formats at bring-up, and a UYVY payload needs explicit support in the capture driver's format table. A sensor that sends it is not enough on its own.
  • Endpoint wiring: the CSI endpoint's lane count and port mapping must match the sensor's lanes. A mismatch gives no frames or corrupted ones, never a clean N-frame stall.
  • Bandwidth headroom: 960,000 B × 8 bit/B × 50 = 384 Mbit/s of payload against two lanes at 500 Mbit/s = 1,000 Mbit/s, about 2.6× of headroom before packet overhead. This link was never the constraint.

Two other DQBUF failures look similar and are not. Error 19, 'No such device', means the device node vanished mid-stream — the USB camera drop signature. Frames that never arrive while the sensor probes fine point at the data path — separating I2C from MIPI failures walks that split.

The one-command discriminator: run a plain v4l2-ctl mmap stream at the same buffer count. If it stalls at the same frame count as your program, queue discipline is the cause no matter whose code fills the loop. For the command-line patterns, capturing raw frames from six V4L2 cameras with v4l2-ctl shows the tool driven directly.

Prevention checklist

Seven habits keep a capture pipeline off this page:

  • Queue every index you requested: VIDIOC_REQBUFS reserves, each VIDIOC_QBUF delivers one buffer into the queue.
  • Compute N ≥ T_worst × fps + 2 from your own worst loop time, measured, not guessed.
  • Keep DQBUF → dispatch → QBUF empty of disk, network and GUI work.
  • Re-queue flagged-error frames; count them instead of hoarding them.
  • Bound DQBUF with a poll() or select() timeout so a dead channel is logged, not hung.
  • On a stall: STREAMOFF, requeue all buffers, STREAMON, and log the lost frames.
  • Rerun the count test after every driver or loop change: a healthy loop never reaches its runway.

When the usual fixes are wrong

  • A sleep in the loop: it throttles to about 9 fps and drops the rest silently. Use it once as a diagnostic, never in production.
  • More buffers: 25 instead of 5 stretches runway from 0.1 s to 0.5 s and does nothing else; the average cycle still binds the rate.
  • New sensor parameters: the lane-bandwidth check already passed with 2.6× headroom, and the moving stall count already cleared the timing. Tuning the sensor wastes a day.

The fix is discipline, not hardware: hand every buffer back within one frame period, queue for the worst cycle, bound every wait. If you are also choosing cameras for the capture chain, the area scan cameras and board-level cameras catalogs, the camera finder and the camera configurator are the starting points, and contact the applications team when a bring-up stalls. The code-reading topic page collects the rest of the capture-pipeline fixes.

Questions

How many V4L2 buffers should a 50 fps capture loop queue?

Size them from your worst loop time: N ≥ worst cycle × fps + 2. A tight 15 ms cycle needs 0.015 × 50 = 0.75, rounded up, plus 2, so 3 buffers; a 60 ms worst case needs 5. Extra buffers only bridge bursts — the average cycle must still stay under the 20 ms frame period for the full 50 fps.

Why does VIDIOC_DQBUF return EAGAIN in non-blocking mode?

With O_NONBLOCK, DQBUF does not wait for a completed frame; it returns -1 with EAGAIN whenever the outgoing queue is empty. An occasional EAGAIN is normal jitter. EAGAIN on every call, starting after roughly as many frames as you queued, means nothing is completing: the incoming queue is dry, so re-queue what you hold and expect to recover with STREAMOFF and STREAMON.

Does a usleep in the capture loop fix DQBUF stalls?

Only as a diagnostic. If a sleep makes capture continuous at a lower rate, it confirms the backlog theory: the loop could not keep up. It also throttles the loop far below the sensor rate, drops frames without reporting errors, and adds the sleep to every frame's latency. Ship the worker-thread fix, not the sleep.

Can sensor settings like pixel clock or MIPI lane rate cause this stall?

Usually not, and the count test tells you first: a stall that moves with the buffer count is queue starvation, while timing faults give no frames, split or shifted lines, or a stall fixed at frame 1 whatever you queue. Check lane bandwidth only after the count test: payload bit rate against lanes times lane rate, with packet overhead in mind.