Skip to content

esp32s3_4827s043c_factory

The esp32s3_4827s043c_factory module is the factory/recovery partition application for the ESP32-S3 4827S043C board — a 4.3″ capacitive-touch display panel based on the ILI9485 RGB controller and the GT911 I2C touch IC. It provides a self-contained, touch-navigable firmware recovery menu that can reflash the main application OTA partitions and the ui_resources data partition directly from a microSD card. Because this board variant ships with no physical buttons, no buzzer, and no rotary encoder (all pins are GPIO_NUM_NC), all navigation is driven exclusively by three virtual on-screen button regions rendered at the bottom of the display.


  1. Module Position in the System
  2. Hardware Context
  3. Module Architecture
  4. Factory Application (Factory/main/)
  5. Graphics Layer (gfx.c)
  6. Peripheral Drivers
  7. Board Support Package (components/bsp/)
  8. Build System (build_scripts/)
  9. Flash Map and OTA Layout
  10. Data Flow Diagrams
  11. Related Modules

This module is a child of the esp32s3_4827s043c board target and is one of three sub-modules that constitute the full board package:

esp32s3_4827s043c

(board root)

esp32s3_4827s043c_factory

(this module - Factory/main/)

esp32s3_4827s043c_bsp

(components/bsp/)

esp32s3_4827s043c_build_scripts

(build_scripts/)

The factory app is separate from the main firmware. It resides in the ESP32-S3’s factory OTA partition and is entered only via the software [ESP444]FACTORY trigger issued by the main firmware (see esp444.cpp). There is no custom bootloader hook on this board — the board has no physical button to hold during power-on, so ENABLE_CUSTOM_BOOT_LOADER is OFF in CMakeLists.txt. The main firmware itself saves the OTA partition state before switching the boot target.

Contrast with boards that do have a custom bootloader hook (e.g. pibot_pendant_v1_0, ESP32_S3_WROOM_CAM), where the user can hold a physical button at power-on to enter recovery — see pibot_pendant_v1_0_bootloader.md.


FeatureValue
SoCESP32-S3 (dual-core Xtensa LX7, 240 MHz)
Display panelILI9485 — 480 × 272 physical (RGB parallel)
Logical orientation90° rotated → 272 × 480 portrait canvas
Touch ICGoodix GT911 (capacitive, I2C)
Physical buttonsNone — all GPIO_NUM_NC
Rotary encoderNone — all GPIO_NUM_NC
BuzzerNone — GPIO_NUM_NC
SD cardSPI (dedicated SPI host)
PSRAMYes — LVGL draw buffers allocated in SPIRAM

The panel is physically mounted at 90° inside the enclosure. The gfx coordinate system treats the screen as 272 px wide × 480 px tall (portrait). All layout constants in main.c are scaled from a 320 × 480 reference (other boards such as esp32_3248s035r) by a factor of 272/320 ≈ 0.85. Border and outline pixel-thickness values (1–2 px insets) are left unscaled.


ESP-IDF APIs

Shared Hardware Drivers - hardware/common/drivers/

Board Support Package - components/bsp/

Factory Application - Factory/main/

main.c

(app_main, menu, actions)

gfx.c

(pixel drawing, snapshot)

touch.c

(GT911 polling)

sdcard.c

(SPI mount/unmount)

buttons.c

(GPIO input, debounce)

buzzer.c

(bit-bang beep)

encoder.c

(PCNT quadrature)

factory_log.h

(log gate)

board_init.c

(board_init, init_lvgl, etc.)

control_event.h

(control_event_t)

control_types.c

(control_events_init)

touch_gt911

bus_i2c

disp_ili9485

(RGB parallel)

disp_backlight

esp_ota_ops

esp_flash / esp_partition

esp_vfs_fat + sdspi

driver/pulse_cnt

esp_lcd (RGB panel)


app_main() executes the following startup sequence before entering the main event loop:

No

Yes

Yes

No

app_main()

factory_log_silence_sd_stack()

restore_otadata_from_backup()

ili9485_init()

Init OK?

abort()

ili9485_backlight(true) + gfx_init()

buttons_init()

encoder_init()

touch_init()

buzzer_init()

Detect partitions

(has_app1?)

Build menu items

probe_sd_files()

draw_menu()

ENABLE_SNAPSHOT?

gpio_config GPIO0

(BOOT button = snapshot trigger)

Main event loop

On this board, buttons_init(), buzzer_init(), and encoder_init() all detect GPIO_NUM_NC pins and return immediately without configuring any GPIO. The display and touch are the only active peripherals.

The main firmware (see esp444.cpp) writes a backup of the two OTA data entries to a reserved 4 KB sector before switching the boot partition to factory. This backup lives at OTADATA_BACKUP_OFFSET = 0xB000, safely between the bootloader end and the partition table at 0xC000. A 32-bit magic word (0xAA55AA55) at +0x40 marks a valid backup.

restore_otadata_from_backup() runs at the very start of app_main(), before any display or peripheral initialization. The algorithm is:

No

Yes

Yes

No

Read magic at BACKUP+0x40

magic == 0xAA55AA55?

Return false

Read 2 × 32-byte entries

from OTADATA_BACKUP_OFFSET

Both entries 0xFF?

Erase OTADATA_BACKUP_OFFSET

(prevent double restore)

Erase OTADATA sectors

(0x10000 + 0x11000)

Write non-empty entries

back to OTADATA

Critical constraint: esp_flash_erase_region(NULL, 0xB000, …) targets an address below the first VFS partition. ESP-IDF’s default CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS rejects this. The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y. This is intentional — the factory app is a privileged recovery tool with direct flash access.

When porting this board to a new flash layout, OTADATA_BACKUP_OFFSET must be recalculated and kept in sync with esp444.cpp in the main firmware.

The menu is a statically-allocated array of menu_item_t structs built at startup:

typedef struct {
const char *label;
menu_action_t action;
uint16_t color; /* text color when not selected */
} menu_item_t;

Available actions (menu_action_t):

ActionDescription
MENU_ACTION_BOOT_APP0Set boot partition to app0 and reboot
MENU_ACTION_BOOT_APP1Set boot partition to app1 and reboot (shown only if app1 exists)
MENU_ACTION_SD_UPDATE_APP0Flash esp3dfw.bin from SD → app0
MENU_ACTION_SD_UPDATE_APP1Flash esp3dfw.bin from SD → app1 (if app1 exists)
MENU_ACTION_SD_UPDATE_RESFlash ui_resources.bin from SD → ui_resources partition

The SD is probed at startup and re-probed after any failed flash operation (probe_sd_files()). SD indicator labels FW (green) and RES (cyan) appear in the header area when the matching .bin file is detected on the card.

Menu navigation functions:

  • menu_select(index) — moves selection, redraws both old and new highlighted items, clears any active status message.
  • menu_move(direction) — wraps-around navigation (+1 / -1), calls menu_select.
  • execute_selected_action() — dispatches the currently highlighted action.

Layout constants are scaled to the 272 × 480 logical canvas (× 0.85 from the 320 × 480 reference boards):

ConstantValueRole
MENU_START_Y94Y of first menu item
MENU_ITEM_H34Height per item
MENU_PAD_X20Horizontal padding
STATUS_YSCREEN_HEIGHT − 28 − BTN_HINT_HStatus text Y
BTN_HINT_H2×22 + 9 = 53Height of virtual button bar

The screen layout from top to bottom:

┌──────────────────────────────────┐ ← y=0
│ "Recovery vX.Y" (header) │
│ ────────────────────────────── │ ← y=40
│ Active: app0 SD: FW RES│ ← y=45 / y=68
│ ────────────────────────────── │ ← y=88
│ [ Boot app0 ] │ ← MENU_START_Y=94
│ [ SD -> app0 ] │
│ [ SD -> resources ] │
│ ────────────────────────────── │
│ Power off to cancel (status) │ ← STATUS_Y
│ ══════════════════════════════ │ ← BTN_HINT_BASE_Y
│ ▲ ▼ ✓ │ ← virtual button bar
└──────────────────────────────────┘ ← y=479

Because no physical buttons exist, three virtual button regions are drawn at the bottom of the screen. Each occupies the full bar height and one-third of the screen width, making the effective tap target far larger than the drawn circle icon.

Virtual Button Bar - BTN_HINT_BASE_Y to SCREEN_HEIGHT

Column 0 .. col_w-1

▲ UP (BTN_1)

Column col_w .. 2×col_w-1

▼ DOWN (BTN_2)

Column 2×col_w .. SCREEN_WIDTH

✓ OK (BTN_3)

touch_hint_hit_test(x, y) maps a touch coordinate to BTN_1 / BTN_2 / BTN_3 / BTN_NONE. Touches above BTN_HINT_BASE_Y always return BTN_NONE.

Touch input in the main loop uses edge detection (touch_was_pressed flag) to fire only on the leading edge of a press:

dispatch_button()gfx / buzzertouch_hint_hit_test()touch_read()Main loopdispatch_button()gfx / buzzertouch_hint_hit_test()touch_read()Main looppollpressed=true, x, yhit test (x, y)vbtn (BTN_1/2/3)draw_button_hint_pressed(vbtn)buzzer_beep_short() - no-op on this boardvTaskDelay 80 msdraw_button_hints() - restore colorsdispatch_button(vbtn)

dispatch_button() is the single unified handler for both physical buttons (when present on other boards) and virtual touch buttons:

ButtonAction
BTN_1menu_move(-1) — scroll up
BTN_2menu_move(+1) — scroll down
BTN_3execute_selected_action() — confirm

action_sd_update(target_label) flashes esp3dfw.bin from the SD card to the named OTA partition using the standard ESP-IDF OTA API:

No

Yes

No

Yes

No

Yes

No

Yes

read/write error

action_sd_update(label)

esp_partition_find_first → target_part

Found?

show_status: Partition not found

sdcard_mount()

OK?

show_status: No SD card!

fopen(FW_FILENAME)

File exists?

show_status: No esp3dfw.bin

fseek → get file size, validate

size valid?

show_status: Invalid size

draw_flashing_screen()

esp_ota_begin()

Read+write loop (1 KB chunks)

draw_progress() per changed percent

esp_ota_end()

esp_ota_set_boot_partition()

rename esp3dfw.bin → esp3dfw.ok

sdcard_unmount()

vTaskDelay 2 s → esp_restart()

esp_ota_abort()

rename → esp3dfw.bad

probe_sd_files() + draw_menu()

After a successful flash, the source file is renamed to esp3dfw.ok on the SD card. On failure it becomes esp3dfw.bad. This allows the operator to distinguish a successful flash from one that failed without removing the card.

action_sd_update_res() writes ui_resources.bin from the SD card to the ui_resources data partition. Unlike firmware flashing it uses esp_partition_erase_range + esp_partition_write directly (no OTA handle), since ui_resources is a raw data partition, not an OTA app slot.

The binary begins with a 16-byte build header ("ESP3" + 12-char variant string) generated by generate_resources.py. The factory app reads and logs this header to help diagnose variant mismatches, but does not block the flash on a mismatch — the operator is expected to verify the correct binary beforehand.

Post-flash, the file is renamed to ui_resources.ok or ui_resources.bad using the same convention as firmware flashing.

When ENABLE_SNAPSHOT is defined at compile time, pressing GPIO0 (the ESP32-S3 BOOT button, always physically present on the module) triggers a full screen capture to the SD card:

No

Yes

No

Yes

Yes

No

snapshot_check()

GPIO0 low?

return immediately

vTaskDelay 50 ms debounce

Still low?

snapshot_take()

sdcard_mount()

snap_find_next_number()

(scan snap000.raw .. snap999.raw)

gfx_snapshot_begin(snapNNN.raw)

In flash operation?

draw_flashing_screen() + draw_progress()

draw_menu() + show_status()

gfx_snapshot_end()

Wait GPIO0 release + debounce

The raw file format is: uint32_t width + uint32_t height (little-endian) followed by native RGB565 pixels in row-major order. A companion Python tool snap2png.py (found in other boards’ Factory/tools/raw2png/) converts these files to PNG. This board’s factory app does not currently ship that tool, but the file format is identical.

Snapshot checks are placed at strategic points in the flash loop (once per progress percent) and at the result screen, so the full flash operation can be captured visually.


The GFX layer is a lightweight, stateless drawing API that operates on a single uint16_t line_buf[SCREEN_WIDTH] static line buffer. All drawing is done line by line to minimise stack and heap usage on the memory-constrained ESP32-S3.

ENABLE_SNAPSHOT only

main.c

draw_* functions

gfx.c API

gfx_clear, gfx_hline, gfx_vline,

gfx_rect, gfx_fill_rect,

gfx_draw_char, gfx_draw_string

gfx_flush() - internal wrapper

ili9485_flush()

(SPI → panel)

snap_write()

(SD card pixel writer)

All pixel data is byte-swapped before transmission (the ILI9485 expects big-endian RGB565 over SPI). snap_write() un-swaps the bytes back to native RGB565 when writing to the SD card snapshot file, seeking to the correct file offset for each row.

GFX API summary:

FunctionDescription
gfx_init()No-op on this board (reserved hook)
gfx_clear(color)Fill entire screen with one color, line by line
gfx_hline(x, y, w, color)Horizontal line, clipped to screen bounds
gfx_vline(x, y, h, color)Vertical line (single-pixel flush per row)
gfx_rect(x, y, w, h, color)Outline rectangle (four calls to h/vline)
gfx_fill_rect(x, y, w, h, color)Filled rectangle, line by line
gfx_draw_char(x, y, c, fg, bg)8 × 16 bitmap glyph from font8x16
gfx_draw_string(x, y, str, fg, bg)String rendering via repeated gfx_draw_char
gfx_snapshot_begin(filepath)Open .raw file, write 8-byte header, pre-fill black
gfx_snapshot_end()Flush and close snapshot file
gfx_snapshot_is_capturing()Returns true while a capture is in progress

touch.c is a thin wrapper over the shared hardware/common/drivers/touch_gt911 component — the same driver used by the main firmware BSP. No re-implementation occurs here; the shared driver handles I2C communication, coordinate swap/invert flags from hw_config.h, and auto-detection of the panel’s native max coordinates from the GT911 device registers.

typedef struct {
bool pressed;
int16_t x; /* display pixel column */
int16_t y; /* display pixel row */
} touch_point_t;

touch_read() calls touch_gt911_read() and copies the result directly. No additional coordinate rescaling is needed because the GT911 driver applies swap/invert flags internally.

sdcard.c mounts the SD card via esp_vfs_fat_sdspi_mount() at /sdcard. It keeps the SPI bus initialized across unmount/remount cycles to avoid conflicts with the TFT SPI peripheral — spi_bus_free() is deliberately not called in sdcard_unmount(). The bus is initialized only once (guarded by a spi_bus_inited flag).

Expected files at the SD card root:

FilenameRolePost-flash rename
esp3dfw.binMain firmware binary→ esp3dfw.ok (success) / esp3dfw.bad (failure)
ui_resources.binUI resources partition image→ ui_resources.ok / ui_resources.bad

buttons.c implements GPIO polling with 50 ms debounce. On this board all three button pins are GPIO_NUM_NC, so buttons_init() detects an all-zero pin_mask and skips gpio_config() entirely. button_wait_press(timeout_ms) polls every 20 ms and returns BTN_NONE on timeout. The code is structurally identical to pibot_pendant_v1_0 so it works unchanged if this board variant ever has physical buttons populated.

buzzer.c uses bit-bang square wave generation at 2700 Hz for a 40 ms duration (buzzer_beep_short()). On this board BUZZER_PIN == GPIO_NUM_NC; both buzzer_init() and buzzer_beep_short() return immediately after the GPIO_IS_VALID_GPIO() guard. Call sites in main.c are not conditionalised — they remain unchanged regardless of board capability.

encoder.c uses the ESP32-S3 PCNT peripheral for quadrature decoding (4 pulses/detent, 1000 ns glitch filter). encoder_init() checks GPIO_IS_VALID_GPIO() for both ENCODER_A_PIN and ENCODER_B_PIN and skips PCNT setup entirely when they are GPIO_NUM_NC. encoder_read() returns 0 while s_initialized is false. The main loop polls the encoder every ~100 ms (governed by button_wait_press(100)).

The factory app uses a compile-time logging gate independent of the main sdkconfig log level:

#if FACTORY_LOG_LEVEL
#define FACTORY_LOGD(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__)
#else
#define FACTORY_LOGD(tag, fmt, ...) do {} while (0)
#endif

FACTORY_LOG_LEVEL is set by the ENABLE_FACTORY_DEBUG_LOG option in Factory/CMakeLists.txt. Warnings (ESP_LOGW) and errors (ESP_LOGE) are always active regardless of this gate.

factory_log_silence_sd_stack() — called at the very start of app_main() — suppresses chatty runtime logs from sdmmc, vfs_fat_sdmmc, sdspi, fatfs, and related tags via esp_log_level_set(). This is separate from the sdkconfig-level suppression applied to early-boot IDF messages (emitted before app_main() runs and therefore not suppressible at runtime).


7. Board Support Package (components/bsp/)

Section titled “7. Board Support Package (components/bsp/)”

The BSP is used by the main firmware, not by the factory app (which uses its own minimal peripheral drivers described in §6). It is documented here because it defines the canonical hardware configuration for this board.

7.1 Display Initialization — ILI9485 RGB

Section titled “7.1 Display Initialization — ILI9485 RGB”

board_init() initialises the system in the following order:

board_init

activity_manager_init()

backlight_configure() + backlight_set(0)

disp_ili9485_configure()

init_touch_controller()

init_lvgl()

backlight_set(BACKLIGHT_DEFAULT_LEVEL_PCT)

control_events_init()

The ILI9485 is driven via the ESP-IDF esp_lcd_rgb_panel API through the shared disp_ili9485 driver from hardware/drivers_video_rgb/. See display_rgb_drivers.md for driver-level details.

The RGB panel shares a single frame buffer between the CPU and the display controller. To eliminate tearing, the LVGL flush callback synchronises with the panel’s VSYNC interrupt using a pair of FreeRTOS binary semaphores:

disp_on_vsync_event() [ISR]lvgl_flush_cb()LVGL render task (Core 1)disp_on_vsync_event() [ISR]lvgl_flush_cb()LVGL render task (Core 1)flush callback invokedoptional snapshot write (ENABLE_SNAPSHOT)RGB565 byte swap (DISPLAY_SWAP_COLOR_FLAG)xSemaphoreGive(sem_gui_ready)xSemaphoreGive(sem_vsync_end) - fires on VSYNCesp_lcd_panel_draw_bitmap()lv_display_flush_ready()

sem_gui_ready signals the ISR that the CPU render is complete; sem_vsync_end signals the flush callback that VSYNC has fired and it is safe to push the buffer to the panel.

init_lvgl() performs the following setup:

  1. Calls lv_init()
  2. Creates sem_vsync_end and sem_gui_ready semaphores
  3. Registers disp_on_vsync_event as the RGB panel VSYNC callback
  4. Creates the LVGL display object at DISPLAY_WIDTH_PX × DISPLAY_HEIGHT_PX
  5. Allocates one or two draw buffers from SPIRAM (MALLOC_CAP_SPIRAM)
  6. Configures LV_COLOR_FORMAT_RGB565, partial render mode, and lvgl_flush_cb
  7. Starts the LVGL tick esp_timer (period: LVGL_TICK_PERIOD_MS)
  8. Creates and registers the GT911 touch input device with a 10 ms poll period

touch_read_cb() maps GT911 native coordinates to display pixel coordinates using a ratio against touch_gt911_get_x_max() / touch_gt911_get_y_max(), then forwards the result to LVGL as LV_INDEV_STATE_PRESSED or LV_INDEV_STATE_RELEASED.

It also integrates with the activity manager: the first touch that wakes the display from a screen-timeout sleep is consumed for the wake-up event and not forwarded to LVGL, preventing accidental UI activation.

control_events_init() registers three board-specific LVGL event codes using lv_event_register_id():

SymbolRole
LV_EVENT_SWITCH_PRESSEDPhysical switch press (not populated on this board)
LV_EVENT_SWITCH_RELEASEDPhysical switch release
LV_EVENT_POTENTIOMETER_CHANGEDAnalog potentiometer change (not populated)

These are carried in control_event_t payloads through the LVGL event system. On this board, only touch events are active; switch and potentiometer events are reserved for future hardware variants that may populate those components.

typedef struct {
lv_indev_t *indev;
uint32_t btn_id;
lv_indev_type_t type;
control_family_t family_id;
int32_t steps;
uint32_t press_duration; /* milliseconds */
} control_event_t;

The build scripts orchestrate all ESP-IDF builds for this board’s firmware variants. They are Python wrappers around idf.py that inject per-variant CMake arguments.

Yes

No

Yes

No

build_one.py main()

Parse argv

(variant name, --clean, --check)

Look up name in

VARIANTS | FACTORY_VARIANTS dict

--check flag?

check_variant(config)

run_cmake_check()

build_variant(config)

--clean flag?

clean_build_dir()

clean installer_dir

generate_resources()

(ui_resources partition)

run_cmake_build()

_show_size_report()

copy_factory_artifacts()

_copy_ui_resources_bin()

_package_user_resources_kit()

generate_flash_map()

make_variant_args() in variants.py constructs the list of CMake -D flags from DEFAULT_OFF_CMAKE_ARGS (all features off by default) plus any feature flags explicitly enabled for the variant — for example ENABLE_SNAPSHOT, ENABLE_FACTORY_DEBUG_LOG, or transport selection flags.


The factory app and the main firmware share a common flash map. Critical addresses:

RegionOffsetNotes
Bootloader0x0000Standard IDF bootloader — no custom hook on this board
OTA data backup0xB000Written by main firmware before switching to factory
Partition table0xC000Standard IDF partition table
NVS0xD000First partition, start of VFS-managed area
OTA data (otadata)0x10000Two 4 KB sectors (entries at +0x0000 and +0x1000)
factory partitionper tableThis recovery app resides here
app0 partitionper tablePrimary main firmware OTA slot
app1 partitionper tableSecondary slot (if present in partition table)
ui_resourcesper tableRaw data partition for UI images, fonts, and themes

The OTA backup sector at 0xB000 satisfies all constraints:

  • After bootloader end
  • Before the partition table (0xC000)
  • Does not overlap any partition (NVS starts at 0xD000)
  • 4 KB-aligned

Porting note: If the bootloader size changes (e.g. when enabling a custom bootloader on a derived board), OTADATA_BACKUP_OFFSET must be recalculated and the new value applied identically in both Factory/main/main.c (factory app) and esp444.cpp (main firmware).


Main Firmware

(running from app0 or app1)

User sends ESP444 FACTORY command

Main firmware: backup otadata → 0xB000

esp_ota_set_boot_partition(factory)

esp_restart()

Factory App boots

(this module)

restore_otadata_from_backup()

Display recovery menu

User selects: Boot appX

User selects: SD → appX

User selects: SD → resources

esp_ota_set_boot_partition(appX)

esp_ota_begin/write/end

+ set_boot_partition(appX)

esp_partition_erase + write

(ui_resources partition)

esp_restart() → main firmware

fread - 1 KB chunks

esp_ota_write()

esp_ota_end() +

esp_ota_set_boot_partition()

esp_restart()

SD Card

esp3dfw.bin

uint8_t buf[1024]

(stack buffer)

NOR Flash

app0 / app1 partition

otadata updated

Next boot: updated firmware


ModuleRelationship
esp32s3_4827s043c_bsp.mdBSP used by the main firmware on this board — defines ILI9485 / GT911 / LVGL integration
esp32s3_4827s043c_build_scripts.mdBuild system orchestration for all firmware variants of this board
pibot_pendant_v1_0_factory_app.mdReference implementation of the same factory pattern with physical buttons, buzzer, encoder, and ILI9341 SPI display
pibot_pendant_v1_0_bootloader.mdCustom bootloader hooks providing button-hold entry into recovery — not used on this board
display_rgb_drivers.mdShared disp_ili9485 RGB driver used by the BSP
touch_drivers.mdShared touch_gt911 driver used by both the factory app (touch.c) and the BSP (board_init.c)
esp32s3_8048s043c_factory.mdParallel board with 800 × 480 RGB panel (ST7262) — same factory pattern, different display
esp32s3_8048s070c_factory_app.md7″ RGB panel variant — same factory pattern