Skip to content

pibot_pendant_v1_0_factory_app_storage

SD card storage module for the PiBot Pendant v1.0 factory application. Provides the two-function interface (sdcard_mount / sdcard_unmount) that the factory app uses to read firmware and resource binaries from an SD card and stream them into flash partitions via the ESP-IDF VFS FAT layer.

Cross-board context: Every supported board has its own sdcard.c with the same sdcard_mount / sdcard_unmount API. The generic behaviour and design rationale shared by all boards is documented in factory_sdcard.md. This document covers PiBot Pendant v1.0-specific details: exact hardware wiring, pin assignments, the two-bus topology, and how the module fits inside the PiBot factory-app sub-module tree.


  1. Module Purpose
  2. Position in the Factory App
  3. Architecture & Components
  4. Public API
  5. Hardware Configuration
  6. SPI Bus Lifecycle
  7. Mount / Unmount State Machine
  8. Data Flow — SD Operations in the Factory App
  9. Logging Control
  10. Design Decisions & Constraints
  11. File Inventory

The factory application for the PiBot Pendant v1.0 is a self-contained, minimal ESP-IDF app stored in a dedicated factory partition. Its primary job is to allow field flashing of the main pendant firmware and the UI-resource binary without requiring a USB cable or an OTA server — the operator copies a binary file onto an SD card and selects the corresponding update action from the factory-app menu.

This storage module owns the entire SD card layer:

  • Initialising the SPI bus once per power-on.
  • Mounting and unmounting the FAT filesystem on demand.
  • Exposing a clean mount / unmount pair so that the rest of the factory app can open files via standard POSIX calls without knowing anything about SPI or VFS configuration.

The module deliberately keeps its scope narrow. It does not read or write files — that responsibility belongs to the callers in main.c (action_sd_update, action_sd_update_res, probe_sd_files), which use standard fopen / fread / fclose calls against the VFS mount point /sdcard.


The factory application is divided into five focused sub-modules. This storage module is the only place that touches the SD card hardware.

sdcard_mount / sdcard_unmount

gfx_* draw calls

button / encoder / touch reads

pibot_pendant_v1_0_factory_app

pibot_pendant_v1_0_factory_app_main

(main.c - menu, OTA flash logic)

pibot_pendant_v1_0_factory_app_display

(gfx.c + ili9341.c - raw framebuffer)

pibot_pendant_v1_0_factory_app_input

(buttons, encoder, touch, buzzer)

⬛ pibot_pendant_v1_0_factory_app_storage

(sdcard.c - this module)

pibot_pendant_v1_0_factory_app_tools

(host-side flash + font scripts)

The bootloader that decides whether to launch the factory app or the main firmware is documented in pibot_pendant_v1_0_bootloader.md.


Board Support

ESP-IDF Framework

Factory App Storage Module

sdcard.c

esp_vfs_fat

FAT over VFS

sdspi_host

SPI ↔ SDMMC bridge

spi_common

SPI bus driver

sdmmc_cmd

card info helper

hw_config.h

pin + frequency constants

factory_log.h

FACTORY_LOGD macro

+ silence helper

SymbolKindVisibilityPurpose
cardstatic sdmmc_card_t *file-privatePoints to the mounted card descriptor; NULL when unmounted
spi_bus_initedstatic boolfile-privateGuards the one-time SPI bus initialisation
sdcard_mount()functionpublicInitialises the SPI bus (once) and mounts the FAT volume
sdcard_unmount()functionpublicUnmounts the FAT volume; keeps the SPI bus running

Declared in boards/pibot_pendant_v1_0/Factory/main/sdcard.h.

esp_err_t sdcard_mount(void);

Sequence of operations:

  1. Returns ESP_OK immediately if card != NULL — callers may call it speculatively without paying a remount cost.
  2. If spi_bus_inited == false, calls spi_bus_initialize(SD_SPI_HOST, …) with the pin configuration from hw_config.h and max_transfer_sz = 4096. Sets spi_bus_inited = true on success.
  3. Builds sdspi_device_config_t with gpio_cs = SD_CS and host_id = SD_SPI_HOST.
  4. Builds sdmmc_host_t with slot = SD_SPI_HOST and max_freq_khz = SD_FREQ_KHZ.
  5. Calls esp_vfs_fat_sdspi_mount() with a mount config that:
    • sets format_if_mount_failed = false — the factory tool must never silently erase user data
    • limits open files to max_files = 4
    • enables disk_status_check_enable = true to detect card removal between operations
  6. On success, stores the card pointer in card and, if FACTORY_LOG_LEVEL is non-zero, prints card info via sdmmc_card_print_info.

Return values:

ValueMeaning
ESP_OKCard mounted (or was already mounted)
ESP_ERR_* from spi_bus_initializeSPI bus init failed
ESP_ERR_* from esp_vfs_fat_sdspi_mountNo card inserted, bad card, or VFS error

Callers display an error status on any non-ESP_OK return and do not proceed with file I/O. The module does not retry internally.


void sdcard_unmount(void);

If card != NULL, calls esp_vfs_fat_sdcard_unmount(SD_MOUNT_POINT, card) and sets card = NULL. The SPI bus (SD_SPI_HOST) is not freed on unmount. See §6 for the rationale.


All constants are defined in boards/pibot_pendant_v1_0/Factory/main/hw_config.h.

ConstantValueRole
SD_SPI_HOSTSPI3_HOSTESP32 VSPI peripheral
SD_MOSIGPIO_NUM_23SPI MOSI
SD_MISOGPIO_NUM_19SPI MISO
SD_CLKGPIO_NUM_18SPI clock
SD_CSGPIO_NUM_5SD chip-select (active low)
SD_FREQ_KHZ2000020 MHz maximum clock
SD_MOUNT_POINT"/sdcard"VFS path prefix for all file I/O

TFT display SPI bus (SPI2_HOST / HSPI) — for reference

Section titled “TFT display SPI bus (SPI2_HOST / HSPI) — for reference”
ConstantValue
TFT_HOSTSPI2_HOST
TFT_MOSIGPIO_NUM_13
TFT_CLKGPIO_NUM_14
TFT_MISOGPIO_NUM_12
TFT_CSGPIO_NUM_15

The PiBot Pendant v1.0 wires the SD card and TFT to different ESP32 SPI peripherals (SPI3_HOST vs SPI2_HOST) on different GPIO pins — they are electrically independent. This distinguishes the PiBot from boards where both devices share a single SPI host. See §6 for why SPI3_HOST is still kept alive after unmount.


SPI2_HOST - TFT (HSPI)SPI3_HOST - SD (VSPI)sdcard.cFactory App (main.c)SPI2_HOST - TFT (HSPI)SPI3_HOST - SD (VSPI)sdcard.cFactory App (main.c)Initialised by ili9341_init() at boot - fully independentspi_bus_inited = trueVFS + SDMMC host layer removedSPI3 bus driver stays alivespi_bus_inited stays truesdcard_mount()spi_bus_initialize() [first call only]esp_vfs_fat_sdspi_mount()card handleESP_OKfopen / fread / fclose via VFSsdcard_unmount()esp_vfs_fat_sdcard_unmount()sdcard_mount() [second call, e.g. after probe_sd_files]esp_vfs_fat_sdspi_mount() [skips spi_bus_initialize]card handleESP_OK

Why the SPI bus is never freed:

Although SPI3_HOST and SPI2_HOST are separate peripherals on this board, calling spi_bus_free() and then spi_bus_initialize() on every mount/unmount cycle adds unnecessary overhead and risks disrupting ESP-IDF’s internal DMA channel bookkeeping. Keeping SPI3_HOST alive means subsequent sdcard_mount calls skip bus initialisation and go straight to esp_vfs_fat_sdspi_mount, which is faster and avoids any risk of IDF internal-state corruption. The spi_bus_inited flag ensures spi_bus_initialize is called at most once per power-on regardless of how many mount/unmount cycles occur.

The same code pattern runs on boards where TFT and SD do share a single SPI host — on those boards freeing the bus would break the display immediately. The PiBot’s two-bus layout makes the constraint less severe but the conservative design is retained for portability and safety.


power-on\nspi_bus_inited = false\ncard = NULL

sdcard_mount()\nspi_bus_initialize() OK\nspi_bus_inited = true

sdcard_mount()\nspi_bus_initialize() FAILS\n(state unchanged)

sdcard_mount()\nesp_vfs_fat_sdspi_mount() OK\ncard != NULL

sdcard_mount()\nmount FAILS (no card / bad card)\ncard stays NULL

sdcard_mount() called again\ncard != NULL → return ESP_OK\n(no-op)

sdcard_unmount()\nesp_vfs_fat_sdcard_unmount()\ncard = NULL\nSPI3 bus stays alive

sdcard_mount()\nesp_vfs_fat_sdspi_mount() OK\n(skips spi_bus_initialize)

Uninitialized

BusReady

Mounted

Key invariant: spi_bus_inited is never reset to false. Once the SPI bus is up it remains up for the lifetime of the factory-app session.


8. Data Flow — SD Operations in the Factory App

Section titled “8. Data Flow — SD Operations in the Factory App”

Three functions in main.c use this module. All follow the same acquire → use → release pattern, with sdcard_mount and sdcard_unmount bracketing every file I/O session.

esp_partition / esp_otaVFS / FAT (/sdcard)sdcard.cmain.c menuesp_partition / esp_otaVFS / FAT (/sdcard)sdcard.cmain.c menuprobe_sd_files() - called at startup and after failed updatesaction_sd_update() - 'Flash firmware' menu actionloop[1 KiB chunks until EOF]action_sd_update_res() - 'Flash UI resources' menu actionloop[1 KiB chunks until EOF]sdcard_mount()ESP_OKfopen("/sdcard/esp3dfw.bin")FILE* or NULL → sets sd_has_fwfopen("/sdcard/ui_resources.bin")FILE* or NULL → sets sd_has_ressdcard_unmount()sdcard_mount()ESP_OK (bus already up)fopen("/sdcard/esp3dfw.bin", "rb")esp_ota_begin(update_part, fw_size)fread(buf, 1, 1024, file)esp_ota_write(ota_handle, buf, len)draw_progress(percent)fclose(file)esp_ota_end() → esp_ota_set_boot_partition()rename esp3dfw.bin → esp3dfw.ok [on success]sdcard_unmount()esp_restart()sdcard_mount()ESP_OKfopen("/sdcard/ui_resources.bin", "rb")esp_partition_erase_range(ui_resources, 0, size)fread(buf, 1, 1024, file)esp_partition_write(ui_resources, offset, buf, len)draw_progress(percent)fclose(file)rename ui_resources.bin → ui_resources.ok [on success]sdcard_unmount()esp_restart()

Files must be placed at the root of the SD card’s FAT filesystem:

FilenamePurposeRenamed on successRenamed on failure
esp3dfw.binMain pendant firmware (OTA binary)esp3dfw.okesp3dfw.bad
ui_resources.binUI icons / fonts resource partition imageui_resources.okui_resources.bad

Renaming the source file after each operation prevents an accidental re-flash on the next boot while leaving a diagnostic artefact on the card. The .ok / .bad extension lets an operator distinguish a previously successful run from a failed one without needing a UART monitor.

For the ui_resources.bin binary format and how to generate it, see the ui_resources/development.md guide.


sdcard.c emits trace messages via FACTORY_LOGD(TAG, ...) with tag "SDCARD". The macro expands to ESP_LOGI when FACTORY_LOG_LEVEL is non-zero, and to a zero-overhead no-op in production builds:

/* factory_log.h */
#if FACTORY_LOG_LEVEL
#define FACTORY_LOGD(tag, fmt, ...) ESP_LOGI(tag, fmt, ##__VA_ARGS__)
#else
#define FACTORY_LOGD(tag, fmt, ...) do {} while (0) /* compiled out */
#endif

FACTORY_LOG_LEVEL is set at compile time by ENABLE_FACTORY_DEBUG_LOG in Factory/CMakeLists.txt. Hard errors (ESP_LOGE) and warnings (ESP_LOGW) in sdcard.c are not gated by this macro — they remain active in all builds. Card info output (sdmmc_card_print_info) is additionally guarded by #if FACTORY_LOG_LEVEL.

The ESP-IDF SD/FAT driver stack is verbose at INFO level. In debug builds (FACTORY_LOG_LEVEL = 1), the sdkconfig log ceiling is raised to INFO for all tags, causing SD-subsystem chatter to appear even though it adds no value for debugging the factory app’s own logic. factory_log_silence_sd_stack() applies dynamic level overrides at runtime to suppress eight specific internal driver tags:

Silenced tagLayer
sdmmcSDMMC protocol layer
vfs_fat_sdmmcVFS FAT ↔ SDMMC bridge
sdmmc_periphSPI peripheral initialisation
sdmmc_reqSDMMC request handler
sdmmc_commonShared SDMMC helpers
fatfsFatFs library
sdspiSPI ↔ SDMMC host driver
sd_diskioDisk I/O layer

This function is called from main.c before the first sdcard_mount() invocation. It has no effect on the "SDCARD" tag used by sdcard.c itself.

IDF-internal logs emitted before app_main() (SPI flash init, partition table, startup banner) cannot be gated at runtime. They are silenced separately by sdkconfig.prod_log, applied by cmake/targets.cmake when ENABLE_FACTORY_DEBUG_LOG is OFF.


DecisionRationale
format_if_mount_failed = falseA factory tool must never silently erase user data. Mount failure surfaces as a visible display error; the SD card is never reformatted.
max_files = 4The factory app opens at most two files at once. A small limit reduces the FAT layer’s internal file-descriptor table, saving RAM on a memory-constrained device.
disk_status_check_enable = trueDetects card removal between a probe_sd_files call and a subsequent action_sd_update call, so the mount correctly fails rather than operating on stale state.
max_transfer_sz = 4096Matches the 1 KiB read buffer in the flash loop and gives the DMA engine headroom. SPI_DMA_CH_AUTO selects the channel automatically.
SD_FREQ_KHZ = 20000 (20 MHz)Conservative clock well within the reliable range for typical SD cards. Higher speeds are not needed for a one-time firmware flash.
SPI bus never freedAvoids spi_bus_free / spi_bus_initialize overhead on every mount/unmount cycle and eliminates any risk of IDF internal DMA-state disruption. The pattern is also safe on boards that share one SPI host between TFT and SD. See §6.
Re-entrant mount guard (card != NULL)Callers can invoke sdcard_mount without tracking whether the card is already mounted — useful when probe_sd_files and action_sd_update* run back-to-back.
One-shot SPI init guard (spi_bus_inited)spi_bus_initialize is not idempotent: calling it twice on the same host returns an error. The boolean flag ensures it is called exactly once per power-on.

FileRole
boards/pibot_pendant_v1_0/Factory/main/sdcard.cModule implementation — sdcard_mount / sdcard_unmount
boards/pibot_pendant_v1_0/Factory/main/sdcard.hPublic header — declares sdcard_mount / sdcard_unmount
boards/pibot_pendant_v1_0/Factory/main/hw_config.hBoard-level pin and frequency constants (SD_* group, TFT_* group)
boards/pibot_pendant_v1_0/Factory/main/factory_log.hFACTORY_LOGD macro + factory_log_silence_sd_stack()
DocumentRelationship
factory_sdcard.mdCross-board reference for the same sdcard_mount / sdcard_unmount pattern — covers all supported boards, generic design rationale, and the hw_config.h symbol contract
pibot_pendant_v1_0_factory_app_main.mdOnly caller of sdcard_mount / sdcard_unmount; implements probe_sd_files, action_sd_update, action_sd_update_res
pibot_pendant_v1_0_factory_app_display.mdILI9341 display driver that owns SPI2_HOST; context for the two-bus topology described in §5 and §6
pibot_pendant_v1_0_bootloader.mdCustom bootloader that launches the factory app on button press; documents the otadata backup/restore sequence
pibot_pendant_v1_0_factory_app_tools.mdHost-side Python tools (flash_all.py, flash_factory.py) for initial production flashing without an SD card