Skip to content

xtensa/esp32s3: refactor cam driver to use cam_ll HAL layer#18703

Merged
xiaoxiang781216 merged 1 commit intoapache:masterfrom
JianyuWang0623:cam-refactor-to-hal-ll
Apr 11, 2026
Merged

xtensa/esp32s3: refactor cam driver to use cam_ll HAL layer#18703
xiaoxiang781216 merged 1 commit intoapache:masterfrom
JianyuWang0623:cam-refactor-to-hal-ll

Conversation

@JianyuWang0623
Copy link
Copy Markdown
Contributor

Summary

As suggested by @eren-terzioglu in #18692 (comment),
refactor the ESP32-S3 CAM driver to use Espressif's cam_ll_* HAL abstraction
layer instead of direct putreg32/getreg32 register manipulation.

Replace all raw register accesses in esp32s3_cam.c with cam_ll_* inline
functions from the esp_hal_cam component in esp-hal-3rdparty. This reduces
maintenance burden and aligns with Espressif's recommended driver architecture.

Changes:

  • Add lcd_cam_dev_t *hw pointer to the driver private struct
  • Use cam_ll_start/stop/reset/fifo_reset for CAM control
  • Use cam_ll_get_interrupt_status/clear_interrupt_status for ISR handling
  • Use cam_ll_set_recv_data_bytelen for DMA buffer length configuration
  • Use cam_ll_select_clk_src/set_group_clock_coeff for clock setup
  • Use cam_ll_enable_vsync_filter/set_vsync_filter_thres for VSYNC filtering
  • Use cam_ll_enable_vsync_generate_eof/enable_rgb_yuv_convert for capture config
  • Use struct field access for interrupt enable (cam_ll_enable_interrupt
    requires __DECLARE_RCC_ATOMIC_ENV which is not available in NuttX)
  • Add esp_hal_cam include paths to hal.mk

Net result: 68 insertions, 155 deletions — eliminates all putreg32/getreg32
calls from the CAM driver.

Impact

  • No functional change — the driver behavior is identical, only the register
    access method changes from raw addresses to the vendor HAL layer.
  • No impact on build process, documentation, security, or compatibility.
  • Only affects arch/xtensa/src/esp32s3/esp32s3_cam.c and hal.mk.

Testing

Host: Ubuntu 22.04 x86_64, xtensa-esp32s3-elf-gcc

Target: ESP32-S3 (lckfb-szpi-esp32s3, ESP32-S3-WROOM-1-N16R8, PSRAM 8MB OCT)
with GC0308 DVP camera and ST7789 LCD

Build and flash:

$ cd nuttx
$ make -j$(nproc) flash ESPTOOL_PORT=/dev/ttyUSB0
...
Generated: nuttx.bin
...
Hard resetting via RTS pin...

Runtime test — continuous preview:

nsh> camera 0
nximage_listener: Connected
nximage_initialize: Screen resolution (320,240)
Start video this mode is eternal. (Non stop, non save files.)

Runtime test — mirrored preview:

nsh> camera 0 -m
nximage_listener: Connected
nximage_initialize: Screen resolution (320,240)
Start video this mode is eternal. (Non stop, non save files.)

Verified:

  • LCD preview displays correct colors (RGB565X byte-swap path active)
  • Hardware mirror (-m) works correctly
  • MMAP buffers allocated with GDMA alignment (3 video buffers in RING mode)
  • Repeated start/stop cycles — no crash, no resource leak

Replace direct putreg32/getreg32 register accesses in esp32s3_cam.c
with cam_ll_* inline functions from Espressif's esp_hal_cam component.
This reduces maintenance burden by using the vendor-provided HAL
abstraction instead of raw register manipulation.

Changes:
- Add lcd_cam_dev_t *hw pointer to driver struct
- Use cam_ll_start/stop/reset/fifo_reset for CAM control
- Use cam_ll_get_interrupt_status/clear_interrupt_status for ISR
- Use cam_ll_set_recv_data_bytelen for DMA buffer length
- Use cam_ll_select_clk_src/set_group_clock_coeff for clock config
- Use cam_ll_enable_vsync_filter/set_vsync_filter_thres
- Use cam_ll_enable_vsync_generate_eof/enable_rgb_yuv_convert
- Use struct access for interrupt enable (cam_ll_enable_interrupt
  requires __DECLARE_RCC_ATOMIC_ENV not available in NuttX)
- Add esp_hal_cam include paths to hal.mk

Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>
@github-actions github-actions bot added Arch: xtensa Issues related to the Xtensa architecture Size: M The size of the change in this PR is medium labels Apr 10, 2026
@eren-terzioglu
Copy link
Copy Markdown
Contributor

Looks clearer a lot, nice work.

@JianyuWang0623
Copy link
Copy Markdown
Contributor Author

@eren-terzioglu Thanks again for the suggestion — I've been refactoring the ESP32-S3 CAM driver to use the cam_ll_* HAL layer as you recommended.

During testing I found a bug in cam_ll_start() in esp-hal-3rdparty (components/esp_hal_cam/esp32s3/include/hal/cam_ll.h):

The current code writes cam_update before cam_start:

dev->cam_ctrl.cam_update = 1;
dev->cam_ctrl1.cam_start = 1;

cam_update is a self-clearing latch — it captures the current register values at the moment it's written. Since cam_start is still 0 when cam_update fires, the hardware latches cam_start=0. The CAM only appears to work because a previous start may have left cam_start high. On a clean first start, this causes the capture to silently fail (symptom: screen flickering / no frame output).

The fix is to swap the order — set cam_start=1 first, then trigger cam_update to latch it:

dev->cam_ctrl1.cam_start = 1;
dev->cam_ctrl.cam_update = 1;

Does espressif/esp-hal-3rdparty accept external PRs to release/master.b? If so I'll submit one. Otherwise, here's the patch:

$ git show
commit 44ec73727cb0bfefb587246d48095c662e372fd4 (HEAD)
Author: wangjianyu3 <wangjianyu3@xiaomi.com>
Date:   Fri Apr 10 18:14:30 2026 +0800

    hal/cam_ll: fix cam_ll_start register write order

    cam_update is a self-clearing latch that captures the current value of
    cam_start. Writing cam_update before cam_start means the latch fires
    while cam_start is still 0, so the CAM never actually starts on the
    first call — it only works if a previous start left cam_start high.

    Swap the order: set cam_start=1 first, then trigger cam_update to latch
    it. This matches the hardware intent documented in the TRM.

    Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>

diff --git a/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h b/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
index 2b58c45f76f..9f6619cf3dd 100644
--- a/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
+++ b/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
@@ -443,8 +443,8 @@ static inline void cam_ll_set_data_wire_width(lcd_cam_dev_t *dev, uint32_t width
 __attribute__((always_inline))
 static inline void cam_ll_start(lcd_cam_dev_t *dev)
 {
-    dev->cam_ctrl.cam_update = 1;
     dev->cam_ctrl1.cam_start = 1;
+    dev->cam_ctrl.cam_update = 1; // self clear, latches cam_start=1
 }

 /**

@eren-terzioglu
Copy link
Copy Markdown
Contributor

@eren-terzioglu Thanks again for the suggestion — I've been refactoring the ESP32-S3 CAM driver to use the cam_ll_* HAL layer as you recommended.

During testing I found a bug in cam_ll_start() in esp-hal-3rdparty (components/esp_hal_cam/esp32s3/include/hal/cam_ll.h):

The current code writes cam_update before cam_start:

dev->cam_ctrl.cam_update = 1;
dev->cam_ctrl1.cam_start = 1;

cam_update is a self-clearing latch — it captures the current register values at the moment it's written. Since cam_start is still 0 when cam_update fires, the hardware latches cam_start=0. The CAM only appears to work because a previous start may have left cam_start high. On a clean first start, this causes the capture to silently fail (symptom: screen flickering / no frame output).

The fix is to swap the order — set cam_start=1 first, then trigger cam_update to latch it:

dev->cam_ctrl1.cam_start = 1;
dev->cam_ctrl.cam_update = 1;

Does espressif/esp-hal-3rdparty accept external PRs to release/master.b? If so I'll submit one. Otherwise, here's the patch:

$ git show
commit 44ec73727cb0bfefb587246d48095c662e372fd4 (HEAD)
Author: wangjianyu3 <wangjianyu3@xiaomi.com>
Date:   Fri Apr 10 18:14:30 2026 +0800

    hal/cam_ll: fix cam_ll_start register write order

    cam_update is a self-clearing latch that captures the current value of
    cam_start. Writing cam_update before cam_start means the latch fires
    while cam_start is still 0, so the CAM never actually starts on the
    first call — it only works if a previous start left cam_start high.

    Swap the order: set cam_start=1 first, then trigger cam_update to latch
    it. This matches the hardware intent documented in the TRM.

    Signed-off-by: wangjianyu3 <wangjianyu3@xiaomi.com>

diff --git a/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h b/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
index 2b58c45f76f..9f6619cf3dd 100644
--- a/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
+++ b/components/esp_hal_cam/esp32s3/include/hal/cam_ll.h
@@ -443,8 +443,8 @@ static inline void cam_ll_set_data_wire_width(lcd_cam_dev_t *dev, uint32_t width
 __attribute__((always_inline))
 static inline void cam_ll_start(lcd_cam_dev_t *dev)
 {
-    dev->cam_ctrl.cam_update = 1;
     dev->cam_ctrl1.cam_start = 1;
+    dev->cam_ctrl.cam_update = 1; // self clear, latches cam_start=1
 }

 /**

It is fine to open there but we have to ask it into esp-idf team about it. Not sure is it added for some reason. Decision is up to you but merge process will take time because a different team works on it. I will ask the reason to responsible team. Thanks for the report.

@JianyuWang0623 JianyuWang0623 marked this pull request as ready for review April 10, 2026 15:19
@xiaoxiang781216 xiaoxiang781216 merged commit 0885b45 into apache:master Apr 11, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: xtensa Issues related to the Xtensa architecture Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants