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:
- OV2640 produces VSYNC pulse
cam_task(in IDLE) receivesCAM_VSYNC_EVENTcam_tasktransitions IDLE → READ_BUF and callscam_start_framecam_start_framecallsll_cam_start, which writesrx_start=1- I²S DMA fires
in_suc_eofoncerx_eof_numsamples are received cam_task(in READ_BUF) processes EOFs, accumulates frame- 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), schedulesvsync_fall_timerto raise back HIGH at +8 ms, and scheduleseof_timerfor +16 ms (mid-cycle). Self-rearms for +32 ms. -
eof_timer: one-shot, scheduled byvsync_kick_timer. Fires ONE rx_eof_num-sized chunk. Bails silently ifrx_start=0(e.g. during the brief window betweenll_cam_stopandll_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:
- ll_cam_stop (rx_start=0)
- final ll_cam_memcpy of dma_buffer[(cnt % half_count) * half_size]
- push frame
- cam_start_frame → ll_cam_start (rx_start=1)
- 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_DEBUGfor cam_hal in the firmware build soEV-EOF-OVF/FB-OVF/NO-SOIwarnings 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 forframe %u: %u bytes %ux%u fmt=%d— expected behaviour: first frame arrives ~600 ms afterCamera init OK. - If still NULL: check serial for
FB-OVF(frame buffer overflow — fb_size too small for accumulated chunks) orNO-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
- esp32-camera/driver/cam_hal.c::cam_task — full state machine, especially the VSYNC handler at line 369
- esp32-camera/driver/cam_hal.c::cam_verify_jpeg_soi
— checks for
FF D8 FF(3 bytes!), accepts only when offset==0 - esp32-camera/target/esp32/ll_cam.c::ll_cam_start
— sets
rx_eof_num,in_link.addr,in_link.start,rx_start - esp32-camera/target/esp32/ll_cam.c::ll_cam_memcpy
— calls
dma_filterwithlen = dma_half_buffer_size, returnselements = len/4real bytes copied