Skip to content

ESP32-S3-WROOM-CAM Board Support Package

The esp32_s3_wroom_cam module is the Board Support Package (BSP) for the ESP32-S3-WROOM-CAM target. It is a headless (no display, no buzzer) pendant node built around the ESP32-S3 with 16 MB of flash, 8 MB of PSRAM, an integrated camera, and USB OTG. Unlike display-centric siblings such as pibot_pendant_v1_0, this board exposes all diagnostic and status output exclusively over UART/USB-CDC and is intended as a WiFi-connected CNC bridge with optional camera streaming.


  1. Hardware Profile
  2. Module Architecture
  3. Sub-modules
  4. Flash Memory Layout
  5. Boot & Recovery Flow
  6. Build Variants
  7. Build Instructions
  8. Key Differences from Display Boards
  9. Hardware Constraints

PropertyValue
MCUESP32-S3 (dual-core Xtensa LX7)
Flash16 MB SPI
PSRAM8 MB (octal SPI)
ConnectivityWiFi 802.11 b/g/n (Bluetooth excluded — PSRAM conflict on S3)
DisplayNone
TouchNone
BuzzerNone
CameraYes (CAMERA_SERVICE — OV2640 or compatible via esp32_camera)
USBUSB OTG (USB Serial via USB_SERIAL_SERVICE)
Recovery buttonGPIO0 — BOOT button (active LOW, internal pull-up, strapping pin)

PSRAM/BT exclusion: The ESP32-S3 cannot run both 8 MB PSRAM and Bluetooth simultaneously due to RF sharing. This board is WiFi-only by design; BT variants are build-system-forbidden. See cmake/sanity_check.cmake for the enforcement rule.


Hardware Peripherals

ESP32-S3-WROOM-CAM Board

GPIO0 held → recovery

Normal boot

esp3dfw.bin found

OTA write + set_boot

idf.py build

idf.py build (Factory dir)

Build System

variants.py

(VARIANTS + FACTORY_VARIANTS)

build_one.py

(CLI entry point)

common.py

(build_variant / check_variant)

Custom Bootloader

(hooks.c)

Factory Recovery App

(main.c)

BSP Component

(board_init.c)

Main OTA Firmware

(ota_0 / ota_1)

Camera

(esp32_camera)

USB-Serial OTG

(usb_serial)

SD Card

GPIO0

BOOT button


Paths:

  • boards/ESP32_S3_WROOM_CAM/Factory/bootloader_components/custom_bootloader/hooks.c — ESP-IDF bootloader component (used by the factory project build)
  • boards/ESP32_S3_WROOM_CAM/Factory/custom_bootloader/hooks.c — Source copy with full function set

The custom bootloader implements the hardware recovery trigger using ESP-IDF’s bootloader hooks API. There is no display or audio feedback; all logging goes to the ROM debug UART.

FunctionVisibilityPurpose
bootloader_hooks_include()Public (linker anchor)Forces the hook object into the bootloader link
bootloader_before_init()PublicReserved; currently empty
bootloader_after_init()PublicGPIO0 sense and recovery dispatch (main logic)
is_button_pressed(pin)StaticDebounced GPIO read — samples 5× over 25 ms, returns true if ≥ 3 reads are LOW
backup_and_erase_otadata()StaticCopies both otadata sectors to the backup flash sector, then erases otadata
SPI Flashhooks.c (bootloader_after_init)ROM BootloaderSPI Flashhooks.c (bootloader_after_init)ROM BootloaderSecond boot with empty otadata - factory partition selectedalt[Button held longer than 5 s][Button released within 5 s]alt[GPIO0 not pressed - normal boot][GPIO0 pressed - poll for release]bootloader_after_init()Configure GPIO0 (input + pull-up), wait 100 msis_button_pressed(0)?return immediatelyPoll every 10 ms until released or 5 s timeoutreturn (abort recovery, normal boot)Read otadata sector 1 (0x10000, 32 B)Read otadata sector 2 (0x11000, 32 B)Erase backup sector (0xF000)Write entry1 at 0xF000, entry2 at 0xF020, magic 0xAA55AA55 at 0xF040Erase otadata sector 1 (0x10000)Erase otadata sector 2 (0x11000)esp_rom_software_reset_system()
BOOT_BUTTON_PIN = GPIO0
OTADATA_OFFSET = 0x10000 (size 0x2000 — 2 × 4 KB sectors)
OTADATA_BACKUP_OFFSET = 0xF000 (last sector of the NVS region)
BACKUP_MAGIC = 0xAA55AA55
RELEASE_TIMEOUT_US = 5,000,000 (5 s)
POLL_INTERVAL_US = 10,000 (10 ms)

Why no display/buzzer? The ESP32-S3-WROOM-CAM board has no panel or audio hardware. Compare with pibot_pendant_v1_0_bootloader, which adds beep_confirm() / beep_short() calls to the same recovery flow.


Path: boards/ESP32_S3_WROOM_CAM/Factory/main/main.c

A minimal C application that occupies the factory partition. It runs only when the bootloader selects factory (i.e., after a recovery-triggered otadata erase). Output is UART/USB-CDC only — no LVGL, no display driver.

Yes

No

No

Yes

No

Yes

fail

app_main

restore_otadata_from_backup

Backup magic

0xAA55AA55

found?

Erase otadata partition

Write entry1 at offset 0

Write entry2 at offset 0x1000

Invalidate magic to 0

Skip restore

sdcard_mount

SD mounted?

Log warning

Delay 10 s

/sdcard/esp3dfw.bin

exists?

ESP_ERR_NOT_FOUND

sdcard_unmount

esp_ota_begin on next_update_partition

Stream fread to esp_ota_write

4 KB chunks

esp_ota_end

esp_ota_set_boot_partition

rename to /sdcard/esp3dfw.ok

Delay 3 s

esp_restart - boots new firmware

rename to /sdcard/esp3dfw.bad

Log error

esp_restart - returns to last valid OTA slot

BehaviourDetail
otadata restoreCalled first, before NVS init, to prevent the backup sector being overwritten
Firmware filename/sdcard/esp3dfw.bin
Success markerRenamed to /sdcard/esp3dfw.ok
Failure markerRenamed to /sdcard/esp3dfw.bad (flash error path only)
Chunk size4 096 bytes (stack-allocated static uint8_t buf[4096])
Progress loggingEvery 64 KB via ESP_LOGI
No-update reboot10 s delay then esp_restart() (returns to last valid OTA slot)

main.c

sdcard.h / sdcard.c

(sdcard_mount / sdcard_unmount)

hw_config.h

(board pin constants)

version.h

(VERSION_BOOTLOADER string)

esp_ota_ops

(IDF - OTA write/end/set_boot)

esp_flash

(IDF - raw flash read/write)

esp_partition

(IDF - partition find/erase/write)


Path: boards/ESP32_S3_WROOM_CAM/components/bsp/board_init.c

The BSP is intentionally minimal because this board has no display, touch, or encoder. It exposes a standard board_init.h API that the firmware’s platform layer calls at startup.

SymbolReturnsDescription
board_init()esp_err_tTop-level hardware init — camera (optional), no display
board_get_name()const char *Returns BOARD_NAME_STR from board_config.h
board_get_version()const char *Returns BOARD_VERSION_STR from board_config.h
board_init_usb()esp_err_tStarts the USB serial task (usb_serial_create_task) — guarded by ESP3D_USB_SERIAL_FEATURE
board_deinit_usb()esp_err_tTears down the USB serial task — guarded by ESP3D_USB_SERIAL_FEATURE

Yes

No

Yes

No

board_init

esp3d_log board name

ESP3D_CAMERA_FEATURE

enabled?

esp32_camera_init

with esp3dCameraConfig

Init OK?

esp3d_log_e

non-blocking, continue

return ESP_OK

Camera is non-blocking: A camera init failure does not abort the firmware startup. This allows the firmware to run in degraded mode if the sensor is absent or faulty.

#if ESP3D_USB_SERIAL_FEATURE // USB serial task management
#if ESP3D_CAMERA_FEATURE // Camera sensor init

These map to CMake OPTION() flags (USB_SERIAL_SERVICE, CAMERA_SERVICE) via cmake/features.cmake. See Build_Development_Tools for the feature pipeline.

This board’s BSP is significantly simpler than display-enabled siblings:

BSP FunctionESP32-S3-WROOM-CAMpibot_pendant_v1_0esp32_3248s035c
board_initCamera onlyDisplay + Touch + Encoder + Buzzer + PotentiometerDisplay + Touch
increase_lvgl_tickAbsent✓✓
lvgl_flush_cbAbsent✓✓
touch_read_cbAbsent✓✓
encoder_read_cbAbsent✓✗
board_init_usb✓AbsentAbsent
Camera init✓AbsentAbsent

Path: boards/ESP32_S3_WROOM_CAM/build_scripts/

Three Python files mirror the pattern used across all boards in this repository.

CLI entry point. Accepts a single variant name plus optional flags.

python build_one.py <variant_name> [--clean] [--check]
  • --clean — removes the build directory and installer output, then exits
  • --check — runs idf.py reconfigure (CMake only) without a full build

Shared helpers. Key responsibilities:

FunctionPurpose
build_variant(config)Orchestrates clean → cmake build → size report → factory artifact copy → flash map generation
check_variant(config)Runs CMake reconfigure only (fast sanity check)
copy_factory_artifacts(cmake_args)Copies bootloader/partition binaries from Factory/installer/ESP3D-FACTORY_16MB/ into the variant installer dir
build_config_name(cmake_args)Derives ESP32_S3_WROOM_CAM_16MB_<radio>_<firmware> from CMake flags
generate_flash_map(config)Calls tools/flash_scripts/flash_mgr.py --generate for the installer directory
_ensure_clean_build_dir(build_dir)Clears the build dir if previously built for a different IDF_TARGET
_build_missing_factory(source_dir)Auto-triggers factory build when its artifacts are absent

IDF_TARGET enforcement: All build and check commands hard-set env["IDF_TARGET"] = "esp32s3". This prevents the wrong chip being selected even if the shell environment differs.

Parallel build support: CMAKE_BUILD_PARALLEL_LEVEL is forwarded from a --jobs=N argument passed by build_mgr.py (idf.py has no -j CLI option).

Defines VARIANTS (main firmware) and FACTORY_VARIANTS (factory recovery) dictionaries consumed by build_one.py and the top-level build_mgr.py.

VARIANTS

FACTORY_VARIANTS

bootloader + partition artifacts

copied to variant installer/

factory_16mb

cwd=Factory/

build_dir=Factory/build/factory_16mb

16mb_wifi_grbl

cwd=REPO_ROOT

build_dir=build/16mb_wifi_grbl_cam

16mb_wifi_fluidnc

cwd=REPO_ROOT

build_dir=build/16mb_wifi_fluidnc_cam

16mb_wifi_grblhal

cwd=REPO_ROOT

build_dir=build/16mb_wifi_grblhal_cam

Generates the CMake argument list for a variant. It starts by turning all known board/feature options OFF (to prevent stale CMake cache values leaking across incremental builds), then turns on only the explicitly listed flags:

DEFAULT_OFF_CMAKE_ARGS = []
for option in ROOT_CMAKE_OPTIONS:
DEFAULT_OFF_CMAKE_ARGS.extend(["-D", f"{option}=OFF"])
def make_variant_args(*on_flags):
args = DEFAULT_OFF_CMAKE_ARGS.copy()
for flag in on_flags:
args.extend(["-D", flag])
return args

This ensures deterministic, reproducible builds regardless of the local CMake cache state.


The following layout is derived from Factory/partitions/partitions_16mb.csv and the constants embedded in hooks.c / main.c.

Address Size Label / Purpose
─────────────────────────────────────────────────────────────
0x0000_0000 varies Bootloader (custom hooks compiled in)
0x000D_000 12 KB NVS region (0xD000–0xFFFF)
└─ 0x000F_000 4 KB ← Bootloader backup sector for otadata
0x0001_0000 8 KB otadata (2 × 4 KB sectors)
├─ 0x0001_0000 4 KB otadata sector 1
└─ 0x0001_1000 4 KB otadata sector 2
...
factory partition Factory recovery app (this module)
ota_0 partition Main firmware slot A
ota_1 partition Main firmware slot B (OTA target)
Offset Size Content
──────────────────────────────────────
0x000 32 B otadata entry 1 (copy of sector 1)
0x020 32 B otadata entry 2 (copy of sector 2)
0x040 4 B Magic: 0xAA55AA55 (valid backup marker)
0x044 rest 0xFF (erased flash)

The magic word at 0xF040 is the handshake between the bootloader and the factory app:

  • Bootloader writes the magic after completing the backup → signals factory app to restore
  • Factory app reads the magic, restores entries, then writes 0x00000000 to invalidate

ROM loads custom bootloader

bootloader_after_init()\nGPIO0 configured, 100 ms settle

GPIO0 HIGH (not pressed)

GPIO0 LOW (pressed)

Held > 5 s

Released within 5 s

Normal boot continues

Backup written to 0xF000

otadata sectors erased

Second boot - empty otadata

Bootloader reads otadata

otadata empty → factory selected

otadata valid → OTA slot selected

Reads magic at 0xF040

otadata restored, magic invalidated

esp3dfw.bin found on SD

SD unavailable or no file

OTA write + set_boot_partition

Returns to last valid OTA slot

Boots newly flashed firmware

board_init() → camera + USB serial

PowerOn

BootloaderInit

CheckButton

NormalBoot

WaitRelease

AbortRecovery

BackupOtadata

EraseOtadata

SoftwareReset

SelectPartition

FactoryApp

MainFirmware

RestoreOtadata

MountSD

FlashFirmware

Reboot10s

RebootApp

Running


All variants share the following base configuration:

CMake FlagValue
ESP32_S3_WROOM_CAMON
MEMORY_16_MBON
PSRAM_8_MBON
USB_SERIAL_SERVICEON
WIFI_SERVICEON
ESP3D_AUTHENTICATIONON
SD_CARD_SERVICEON
CAMERA_SERVICEON
MDNS_SERVICEON
WEB_SERVICESON
WEBUI_SERVERON
TIME_SERVICEON
UPDATE_SERVICEON
Variant NameTARGET_FW_*Build DirInstaller Dir
16mb_wifi_grblGRBLbuild/16mb_wifi_grbl_caminstaller/ESP32_S3_WROOM_CAM_16MB_wifi_grbl/
16mb_wifi_fluidncFLUIDNCbuild/16mb_wifi_fluidnc_caminstaller/ESP32_S3_WROOM_CAM_16MB_wifi_fluidnc/
16mb_wifi_grblhalGRBLHALbuild/16mb_wifi_grblhal_caminstaller/ESP32_S3_WROOM_CAM_16MB_wifi_grblhal/
factory_16mbN/AFactory/build/factory_16mbFactory/installer/ESP3D-FACTORY_16MB/

Factory artifacts dependency: All three main variants require the factory variant to be built first. common.py::copy_factory_artifacts() auto-triggers factory_16mb if its output directory is absent.

  • Camera + WiFi: Both run simultaneously. Camera frame buffers are allocated from PSRAM (MALLOC_CAP_SPIRAM) to avoid exhausting DRAM.
  • No Bluetooth: BT is unconditionally excluded by cmake/sanity_check.cmake.
  • WebUI available: WEBUI_SERVER=ON enables the embedded web interface over WiFi. SSDP is OFF for this board.
  • CNC link is USB serial: USB_SERIAL_SERVICE=ON. Socket client/server are both OFF. See the WiFi role/CNC transport model in the architecture overview.

  • The installer directory name comes from CMake (cmake/build_config_name.cmake, which appends the log level _LOG_<LEVEL>): installer_config_name(cmake_args, build_dir) reads <build_dir>/build_config_name.txt and falls back to build_config_name() before any configure; copy_factory_artifacts(cmake_args, build_dir) uses it too.
  • New 16mb_wifi_marlin variant (Marlin groundwork, TARGET_FW_MARLIN=ON, USB serial CNC link, WiFi + WebUI, camera, SD).

  • ESP-IDF v5.4.3 installed and sourced
  • IDF_PATH environment variable set (or update in common.py)
  • Python 3.x
Terminal window
cd boards/ESP32_S3_WROOM_CAM/build_scripts
# Build FluidNC variant
python build_one.py 16mb_wifi_fluidnc
# Build GRBL variant with a clean first
python build_one.py 16mb_wifi_grbl --clean
# CMake-only sanity check (no compile)
python build_one.py 16mb_wifi_grblhal --check
# Build factory recovery app
python build_one.py factory_16mb
Terminal window
idf.py -B build/16mb_wifi_fluidnc_cam flash monitor
  1. Copy the main firmware binary to the SD card root as esp3dfw.bin
  2. Hold GPIO0 (BOOT button) at power-on
  3. Release when the board resets (within 5 s of pressing)
  4. The factory app flashes esp3dfw.bin to ota_0 and reboots
  5. On success, esp3dfw.bin is renamed to esp3dfw.ok on the SD card
  6. On failure, esp3dfw.bin is renamed to esp3dfw.bad for diagnosis

This board deliberately omits subsystems present on all display-based boards:

SubsystemDisplay boardsESP32-S3-WROOM-CAM
Display driver (SPI/RGB/I80)RequiredAbsent
LVGL init + tick timerRequiredAbsent
Touch controllerRequiredAbsent
Buzzer feedback in bootloaderpibot_pendant_v1_0 has itAbsent
Factory app graphical UIFull menus + GFXUART/USB-CDC only
Encoder / potentiometerpibot_pendant_v1_0Absent
CameraAbsent on all display boardsPresent
USB Serial as primary CNC linkRareStandard
PSRAM0–4 MB on others8 MB

The absence of LVGL means none of the UI Framework & Screens subsystem is compiled for this target. board_init.c does not call init_lvgl(), increase_lvgl_tick(), or any display flush callback.


ModeApproximate available DRAM
WiFi active~75 KB (before PSRAM allocations)
BluetoothNot supported

Camera frame buffers are allocated in PSRAM (MALLOC_CAP_SPIRAM). The 8 MB PSRAM headroom makes this board significantly less constrained than DRAM-only targets for camera and web workloads.

GPIO0 is an ESP32-S3 strapping pin. The internal pull-up is enabled by the bootloader hook (gpio_ll_pullup_en). External circuitry connected to GPIO0 must not hold it LOW at power-on unless a factory recovery is intended.

board_init_usb() is called separately from board_init(). The platform layer must call both in sequence:

board_init(); // Camera init (non-blocking on failure)
board_init_usb(); // USB serial task start

USB OTG and the standard UART can both log simultaneously.

  • IDF_TARGET is hard-set to esp32s3 in every subprocess.run() call — external environment overrides are ignored.
  • The _ensure_clean_build_dir() helper writes a .idf_target marker file and clears the build directory on target mismatch, preventing silent cross-target contamination.