velxio/test/test-esp32-cam/autosearch/13_three_remaining_bugs.md

7.3 KiB

13 — Three more bugs found by close-reading cam_hal/ll_cam

Continuation of 12_descriptor_walker_fix.md. While re-reading the upstream cam_hal driver and my I²S device side-by-side I found three additional bugs that explain why fb_get still returns NULL even after the multi-descriptor walker fix.

Bug #2 — pack_two_pixels consumed bytes it didn't deliver

Diagnosis

The walker called next_pixel_byte() twice per sample but only used the first byte; the second was discarded. pack_two_pixels(p1, p2) ignored p2 (commented as "kept for API stability"). Combined with ll_cam_dma_filter_jpeg reading only sample1 from each dma_elem_t, the firmware ended up seeing every other byte of the JPEG with the intermediate bytes silently dropped.

For a JPEG that starts FF D8 FF E0 00 10 4A 46 ... the firmware saw FF FF 00 4A ... — the D8 byte (which is part of the SOI marker FF D8 FF) was skipped, so cam_verify_jpeg_soi failed and cam_task discarded the frame.

Fix

Renamed the helper to pack_one_pixel(p) (single-byte input) and the walker loop now calls next_pixel_byte() exactly once per sample.

static inline uint32_t pack_one_pixel(uint8_t p) {
    return ((uint32_t)p << 16);  /* sample1 field */
}

This is the per-byte mapping the upstream driver actually expects: each 32-bit dma_elem_t in DMA memory carries ONE real JPEG byte in sample1. Walker output: 1 stored 4-byte word ↔ 1 real byte.

Reference

esp32-camera/target/esp32/ll_cam.c::ll_cam_dma_filter_jpeg — filter copies only sample1, returns elements = len/4.

Bug #3 — Chicken-and-egg between VSYNC and rx_start

Diagnosis

The original eof_timer was scheduled by maybe_arm_capture on the rx_start 0→1 edge. The timer was also the only thing firing VSYNC pulses. But rx_start = 1 is written by ll_cam_start, which is called by cam_task only when it receives a CAM_VSYNC_EVENT.

Sequence required by upstream:

  1. OV2640 produces VSYNC pulse
  2. cam_task (in IDLE) receives CAM_VSYNC_EVENT
  3. cam_task transitions IDLE → READ_BUF and calls cam_start_frame
  4. cam_start_frame calls ll_cam_start, which writes rx_start=1
  5. I²S DMA fires in_suc_eof once rx_eof_num samples are received
  6. cam_task (in READ_BUF) processes EOFs, accumulates frame
  7. Next VSYNC closes frame, pushes to frame_buffer_queue

But in my emulation, step 1 (VSYNC) never happened because the timer that fires VSYNC was gated on the result of step 4 (rx_start=1). Deadlock — the firmware sat in IDLE forever and fb_get blocked.

The user's gpio: GPIO[25] ... Intr:2 line in serial output confirms the firmware enabled the interrupt; the driver was ready to receive VSYNC. My device just never sent one.

Fix

Split the single eof_timer into two independent timers:

  • vsync_kick_timer: independent, runs continuously at 30 fps. Each tick pulses VSYNC LOW (NEGEDGE), schedules vsync_fall_timer to raise back HIGH at +8 ms, and schedules eof_timer for +16 ms (mid-cycle). Self-rearms for +32 ms.

  • eof_timer: one-shot, scheduled by vsync_kick_timer. Fires ONE rx_eof_num-sized chunk. Bails silently if rx_start=0 (e.g. during the brief window between ll_cam_stop and ll_cam_start).

The kick timer starts from realize() with first fire at +500 ms — enough time for the firmware to boot and reach esp_camera_init(), but well before the user's setup() calls fb_get (which has a default 5 s timeout).

Stray VSYNC pulses before cam_init registers its GPIO ISR are dropped at the GPIO peripheral level (no ISR registered → edges go nowhere) — harmless.

Why a 30 fps cadence (not 60)

Real OV2640 runs at 30 fps for QVGA-JPEG. The 32 ms VSYNC period gives the firmware plenty of time between VSYNCs to:

  • process VSYNC #N event (state IDLE → READ_BUF, call ll_cam_start, which writes rx_start=1)
  • process EOF (call ll_cam_memcpy, do SOI check, accumulate)
  • be ready for VSYNC #N+1 to close the frame

A 16 ms cadence (60 fps) was tighter and more error-prone.

Bug #4 (potential) — frame_pos reset timing

Diagnosis (suspected — verify with rebuilt DLL)

The walker resets frame_pos = 0 inside vsync_kick_cb BEFORE firing VSYNC. So the next eof_timer fire (16 ms later) reads JPEG bytes starting at offset 0 → firmware sees SOI in chunk 0.

But: cam_task's flow on VSYNC #N is:

  1. ll_cam_stop (rx_start=0)
  2. final ll_cam_memcpy of dma_buffer[(cnt % half_count) * half_size]
  3. push frame
  4. cam_start_frame → ll_cam_start (rx_start=1)
  5. cnt = 0, state = READ_BUF

So when VSYNC #N fires, the firmware does ONE MORE memcpy of previous frame data using cnt=PREVIOUS_LAST_CNT. But by that point my walker has already reset frame_pos=0 (in the same vsync_kick_cb that triggered the VSYNC pulse). The data the firmware is reading is whatever the last EOF wrote — which was filled with JPEG bytes BEFORE the frame_pos reset.

Result: should be fine — the final memcpy reads bytes that were written during the previous EOF cycle (at offsets that match the previous frame's bytes). Then the new frame's first EOF (16 ms after VSYNC #N) reads from frame_pos=0 = JPEG SOI. ✓

I'll verify this once the DLL is rebuilt by watching the cam_hal logs (need to enable ESP_LOG_DEBUG for cam_hal — see TODO below).

TODO before next build

  • Enable ESP_LOG_DEBUG for cam_hal in the firmware build so EV-EOF-OVF / FB-OVF / NO-SOI warnings print to serial. The throttled warnings (CAM_WARN_THROTTLE) silently swallow diagnostic output without it.
  • Once DLL is rebuilt: re-run webcam_demo.ino, watch for frame %u: %u bytes %ux%u fmt=%d — expected behaviour: first frame arrives ~600 ms after Camera init OK.
  • If still NULL: check serial for FB-OVF (frame buffer overflow — fb_size too small for accumulated chunks) or NO-SOI (SOI check failure — frame_pos timing off or descriptor data wrong).

Files changed in this iteration

File Change
third-party/qemu-lcgamboa/hw/misc/esp32_i2s_cam.c Multi-descriptor walker + one-byte-per-sample fix + split timers
third-party/qemu-lcgamboa/include/hw/misc/esp32_i2s_cam.h Added vsync_kick_timer field, updated comments
test/test-esp32-cam/autosearch/12_descriptor_walker_fix.md (previous bug write-up)
test/test-esp32-cam/autosearch/13_three_remaining_bugs.md This file

Sources reread for this iteration