Skip to content

cam_hal: PSRAM JPEG: fall back to full-frame EOI search when the tail probe misses - #871

Open
JohannYellow wants to merge 1 commit into
espressif:masterfrom
JohannYellow:fix/psram-eoi-padded-frames
Open

JohannYellow wants to merge 1 commit into
espressif:masterfrom
JohannYellow:fix/psram-eoi-padded-frames

Conversation

@JohannYellow

Copy link
Copy Markdown

Fixes #853. Likely also the unresolved reports in #775.

What this does

In PSRAM DMA mode (CONFIG_CAMERA_PSRAM_DMA=y) with PIXFORMAT_JPEG, cam_take() only searches the tail of the frame for the JPEG EOI marker: a window of dma_node_buffer_size + 1 bytes (1025 by default), introduced in #777 / #785 to avoid slow PSRAM reads and stale markers from a larger previous frame.

Some sensors pad the frame with zeros past the EOI. The OV3660 appends ~2.2–2.9 kB, so FF D9 consistently lands outside the window, offset_e stays -1, and every frame is dropped:

cam_hal: NO-EOI - JPEG end marker missing
cam_hal: Failed to get frame: timeout

This PR keeps the fast tail probe unchanged and, only when it misses, re-scans the whole frame with the same backward search the non-PSRAM path already uses:

if (offset_e < 0) {
    cam_drop_psram_cache(dma_buffer->buf, dma_buffer->len);
    offset_e = cam_verify_jpeg_eoi(dma_buffer->buf, dma_buffer->len, false);
}

The change is strictly additive: sensors that end the frame at the EOI (e.g. OV2640) never reach the fallback, and the fallback only runs on frames that would otherwise have been discarded.

Why it is safe against stale markers

The tail probe exists partly to avoid picking up an EOI left behind by a larger previous frame. That cannot happen here: in this path len = cnt * dma_half_buffer_size, so all of [0, len) was written by this frame's DMA. Stale bytes only live beyond len. A backward search over [0, len) therefore returns the last marker before the padding, which is this frame's real EOI.

Measured

ESP32-S3, 8 MB octal PSRAM, OV3660, SVGA/JPEG q12, fb_count=2, ESP-IDF 5.4.2. Instrumenting the NO-EOI path with a full-buffer search found the marker on every dropped frame:

len=19456 fb_size=96000 full_eoi_off=17201 tail=00 00 00 00 00 00 00 00   (2253 B padding)
len=18432 fb_size=96000 full_eoi_off=16214 tail=00 00 00 00 00 00 00 00   (2216 B padding)

There is no truncation: the JPEGs are ~17 kB in a 96 kB buffer, the tail is all zeros, and no DMA overflow / NO-SOI warning is ever logged. The failure reproduced the same way on v2.1.4 and v2.1.7, and at both 10 and 20 MHz XCLK. Swapping in an OV2640 with everything else unchanged works.

This also explains the light dependency reported in #775: eoi_probe_window() returns the full frame_len when the frame is ≤ 1025 bytes, so a near-black frame small enough to fit gets a full scan and succeeds, while larger frames get the narrow window.

Tested

Limits

  • This fixes the case where the frame is complete and the EOI is present but padded out of the probe window. It does not help when the EOI is genuinely missing (e.g. data loss under high PSRAM bandwidth). The later OV5640 report in PSRAM DMA + JPEG: every frame rejected as NO-EOI because the EOI probe window is too narrow for sensors that pad past the end marker (measured on OV3660) #853 shows eoi_first=-1 eoi_last=-1 across the whole buffer, which is that other failure mode.
  • When the probe misses, the fallback costs one extra cache invalidate and a backward scan of the frame from PSRAM. That happens on sensors that pad past the EOI (measured on OV3660, and by the OV5640 report above most likely OV5640 too; OV2640 does not pad), and on frames with no EOI at all, which are dropped either way.

Unrelated observation (not changed here)

eoi_probe_window() names its first parameter half, and #785's title talks about the last half-buffer, but the call site passes cam_obj->dma_node_buffer_size. When dma_half_buffer_size > dma_node_buffer_size the window is smaller than intended. Both are 1024 in my configuration, so it is not the cause here.

🤖 Generated with Claude Code

esp_camera_fb_get() discarded every frame with "NO-EOI - JPEG end marker
missing" and timed out. The JPEGs were valid all along - the driver was
looking in the wrong place.

In PSRAM mode cam_take() only scans the last (dma_node_buffer_size + 1) =
1025 bytes of the frame for the EOI marker. The OV3660 pads each frame
with zeros well past the end marker - measured 2218-2903 bytes on an
ESP32-S3 at SVGA/JPEG - so the marker consistently fell outside that
window and every frame was discarded. The OV2640 does not pad, which is
why it was unaffected.

Instrumenting the NO-EOI path showed the marker present every time:
  len=19456 fb_size=96000 full_eoi_off=17201 tail=00 00 00 00 00 00 00 00
  len=18432 fb_size=96000 full_eoi_off=16214 tail=00 00 00 00 00 00 00 00
(~17 kB JPEGs in a 96000-byte buffer, so truncation was never involved.)

When the narrow tail probe misses, re-scan the whole frame with the same
backward search the non-PSRAM path already uses. The fast path is
unchanged for sensors that do not pad. Safe against the stale-frame case
the probe window guards against: len = cnt * dma_half_buffer_size, so all
of [0, len) was written by this frame's DMA - stale bytes only live
beyond len.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AxelLin

AxelLin commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Test with OV5640 / ESP32S3, confirm it works after applying this fix.
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants