Skip to content

esp32s3_zx3d50ce02s_usrc_4832 Factory App — Input Subsystem

esp32s3_zx3d50ce02s_usrc_4832 Factory App — Input Subsystem

Section titled “esp32s3_zx3d50ce02s_usrc_4832 Factory App — Input Subsystem”

The input subsystem of the esp32s3_zx3d50ce02s_usrc_4832 factory application bundles all human-interface device drivers used during manufacturing testing and field recovery: three physical GPIO buttons, a quadrature rotary encoder, an FT5x06 capacitive touch controller, and a bit-banged buzzer for auditory feedback. Every driver is deliberately minimal—no RTOS queues, no interrupts, no dynamic allocation—so the factory app stays deterministic and easy to audit.


  1. Module Context
  2. Architecture Overview
  3. Component Reference
  4. Hardware Configuration
  5. Data Flow and Interaction Diagrams
  6. Process Flows
  7. Design Constraints and Invariants
  8. Cross-Board Pattern
  9. Related Modules

This module is a child of esp32s3_zx3d50ce02s_usrc_4832_factory_app, the standalone factory/recovery application flashed to the ESP32-S3 ZX3D50CE02S-USRC-4832 board before the main pendant firmware.

esp32s3_zx3d50ce02s_usrc_4832_factory_app
├── esp32s3_zx3d50ce02s_usrc_4832_factory_app_main ← orchestration
├── esp32s3_zx3d50ce02s_usrc_4832_factory_app_display ← gfx + ST7796 I80
├── esp32s3_zx3d50ce02s_usrc_4832_factory_app_input ◄ THIS MODULE
├── esp32s3_zx3d50ce02s_usrc_4832_factory_app_storage ← SD card
└── esp32s3_zx3d50ce02s_usrc_4832_factory_app_tools ← flash helpers

The main application loop (see esp32s3_zx3d50ce02s_usrc_4832_factory_app_main) polls every input source; this module provides the four polling APIs it calls.


Physical Hardware

ESP-IDF Drivers

Factory App - Input Subsystem

buttons_init / button_is_pressed

button_wait_press

encoder_init / encoder_read

touch_init / touch_read

buzzer_init / buzzer_beep_short

hw_config.h

Pin assignments

BUTTON_n_PIN

ENCODER_A/B_PIN

TOUCH_*

BUZZER_PIN

buttons.c

GPIO polling + debounce

encoder.c

PCNT quadrature

touch.c

FT5x06 over I2C

buzzer.c

Bit-bang square wave

driver/gpio

driver/pulse_cnt

bus_i2c

(shared component)

touch_ft5x06

(shared component)

esp_rom_sys

(delay)

3x Push Buttons

(active-LOW)

Rotary Encoder

(quadrature A/B)

FT5x06 Touch IC

(I2C)

Buzzer

(passive)

main.c

(factory app loop)

All drivers share the same two-phase lifecycle: init once at startup, then poll from the main loop. No callbacks, no ISRs, no queues.


Purpose: Provides blocking and non-blocking access to three physical push buttons with integrated 50 ms software debounce.

IdentifierDescription
button_id_tEnum: BTN_1, BTN_2, BTN_3, BTN_NONE (defined in buttons.h)

static uint64_t pin_bit(gpio_num_t pin)

Internal helper. Returns 1ULL << pin when pin is a valid GPIO, otherwise 0. Avoids a compile-time negative-shift warning that would arise if GPIO_NUM_NC (negative) were shifted directly in a bit-mask expression.


void buttons_init(void)

Configures all three button GPIOs as inputs with internal pull-ups enabled. Button pins are taken from hw_config.h (BUTTON_1_PIN, BUTTON_2_PIN, BUTTON_3_PIN).

Guard: If all three pins resolve to GPIO_NUM_NC (no physical buttons on this board variant), the function builds a zero mask and returns immediately without touching the GPIO driver.

GPIO config applied:

FieldValue
ModeGPIO_MODE_INPUT
Pull-upGPIO_PULLUP_ENABLE
Pull-downGPIO_PULLDOWN_DISABLE
InterruptGPIO_INTR_DISABLE

bool button_is_pressed(button_id_t btn)

Returns true if the specified button is currently held down (active-LOW logic — a 0 level means pressed). Returns false immediately for an out-of-range button ID or an invalid pin (GPIO_NUM_NC).


button_id_t button_wait_press(uint32_t timeout_ms)

Blocking poll. Scans all buttons in a 20 ms loop until a press is detected or timeout_ms milliseconds have elapsed. Passing 0 for timeout_ms waits indefinitely.

Debounce sequence:

  1. Detect falling edge (GPIO level → 0).
  2. Wait 50 ms (DEBOUNCE_MS).
  3. Confirm still pressed.
  4. Wait for release (level → 1).
  5. Wait another 50 ms post-release de-bounce.
  6. Return the button_id_t.

Returns BTN_NONE on timeout.


GPIO level = 0

wait 50 ms

still pressed

released (glitch)

record button

GPIO level = 1

wait 50 ms

elapsed >= timeout_ms AND timeout_ms != 0

return BTN_NONE

Idle

Detected

Debounce_Wait

Confirmed

Wait_Release

Release_Debounce

Return_ID

Timeout


Purpose: Decodes a quadrature rotary encoder using the ESP32-S3’s hardware Pulse Counter (PCNT) peripheral. Returns the number of detent clicks since the last call; no queue or interrupt is used.

MacroValueDescription
PULSES_PER_DETENT4Raw PCNT edges per physical detent. Adjust if rotation feels too sensitive or sluggish.

esp_err_t encoder_init(void)

Configures two PCNT channels for full quadrature decoding (ENCODER_A_PIN, ENCODER_B_PIN from hw_config.h).

Guard: Returns ESP_OK immediately if either pin is GPIO_NUM_NC. encoder_read() will safely return 0 in that case.

Configuration applied:

SettingValue
PCNT high limit+1000
PCNT low limit-1000
Glitch filtermax_glitch_ns = 1000 (matches main firmware)
Pull-upsInternal pull-ups on both A and B pins
Channel A edgeENCODER_A_PIN: DECREASE on rising, INCREASE on falling
Channel A levelENCODER_B_PIN: HIGH → KEEP, LOW → INVERSE
Channel B edgeENCODER_B_PIN: INCREASE on rising, DECREASE on falling
Channel B levelENCODER_A_PIN: HIGH → KEEP, LOW → INVERSE

int encoder_read(void)

Reads the accumulated PCNT count, converts it to detent clicks (count / PULSES_PER_DETENT), clears the counter, and returns the click count. Returns 0 if the encoder is not initialized or if no full detent has been completed since the last call.

Note: The counter is cleared after every read. Remainder pulses (less than one full detent) are discarded. This avoids accumulation drift at the cost of sub-detent precision, which is acceptable for factory menu navigation.

Return value: positive = clockwise, negative = counter-clockwise (sign depends on physical wiring; swap PCNT_CHANNEL_EDGE_ACTION_INCREASE / DECREASE if reversed).


Channel B

Channel A

Edge: ENCODER_A_PIN

Rising=DECREASE / Falling=INCREASE

Level: ENCODER_B_PIN

HIGH=KEEP / LOW=INVERSE

Edge: ENCODER_B_PIN

Rising=INCREASE / Falling=DECREASE

Level: ENCODER_A_PIN

HIGH=KEEP / LOW=INVERSE

PCNT Counter

Limits: -1000..+1000

Glitch filter: 1000 ns

encoder_read()

count / 4 = clicks


Purpose: Thin wrapper over the shared touch_ft5x06 hardware driver component. Initializes the FT5x06 capacitive touch IC over I2C and returns touch coordinates already rescaled to screen pixels.

/* touch.h */
typedef struct {
bool pressed; /* true if a finger is detected */
int16_t x; /* screen X pixel [0 .. SCREEN_WIDTH-1], or -1 if not pressed */
int16_t y; /* screen Y pixel [0 .. SCREEN_HEIGHT-1], or -1 if not pressed */
} touch_point_t;

void touch_init(void)

Initialises the I2C bus (bus_i2c_init) and configures the FT5x06 controller (touch_ft5x06_configure) using compile-time constants from hw_config.h.

Key design decisions (documented in the file header):

  • Fixed x_max / y_max: Auto-detection is intentionally disabled. The FT5x06 on another board (esp32s3_hmi43v3) was found to report bogus auto-detected values; fixed values from hw_config.h are used proactively to avoid repeating that bug.
  • No independent touch reset: TOUCH_RST_PIN is GPIO_NUM_NC. The touch IC shares the display’s physical reset line (TFT_RST_PIN). The display driver (st7796_i80_init(), called before touch_init() in main.c) already pulses reset, which brings the FT5x06 out of reset as a side effect. touch_init() must therefore be called after display initialisation.
  • On any initialisation error, s_initialized remains false and subsequent touch_read() calls return a safe “not pressed” value without crashing.

FT5x06 configuration fields applied:

FieldSource in hw_config.h
i2c_addrTOUCH_I2C_ADDR_LIST
i2c_portTOUCH_I2C_PORT_IDX
i2c_clk_speedTOUCH_I2C_FREQ_HZ
rst_pinTOUCH_RST_PIN (GPIO_NUM_NC)
int_pinTOUCH_IRQ_PIN
swap_xyTOUCH_SWAP_XY_FLAG
invert_xTOUCH_MIRROR_X_FLAG
invert_yTOUCH_MIRROR_Y_FLAG
x_maxTOUCH_X_MAX (fixed, not auto-detected)
y_maxTOUCH_Y_MAX (fixed, not auto-detected)

touch_point_t touch_read(void)

Calls touch_ft5x06_read() and maps raw controller coordinates to screen pixels:

pt.x = raw_x * SCREEN_WIDTH / touch_ft5x06_get_x_max()
pt.y = raw_y * SCREEN_HEIGHT / touch_ft5x06_get_y_max()

Returns { .pressed = false, .x = -1, .y = -1 } if the driver is not initialized or no finger is detected.


I2C polling

FT5x06 IC

(raw I2C data)

touch_ft5x06_read()

shared hardware driver

Coordinate rescaling

x = raw_x x SCREEN_WIDTH / x_max

y = raw_y x SCREEN_HEIGHT / y_max

touch_point_t

{pressed, x, y}

in screen pixels


Purpose: Provides a short auditory confirmation beep (40 ms at ~2700 Hz) using software bit-banging on a GPIO. No PWM or LEDC peripheral is required.

MacroValueDescription
BEEP_FREQ_HZ2700Square-wave toggle frequency in Hz
BEEP_DURATION_MS40Total beep duration in milliseconds

static uint64_t pin_bit(gpio_num_t pin)

Same guard helper as in buttons.c. Returns a valid GPIO bit mask or 0 for GPIO_NUM_NC. Prevents a compile-time negative-shift warning when constructing gpio_config.pin_bit_mask.


void buzzer_init(void)

Configures BUZZER_PIN as a push-pull output and drives it LOW. If BUZZER_PIN is GPIO_NUM_NC (no buzzer on this board variant), returns immediately.


void buzzer_beep_short(void)

Produces a single short beep by toggling the GPIO at the target frequency using busy-wait delays (esp_rom_delay_us).

Timing calculation:

half_period_us = 1 000 000 / (2 x 2700) = ~185 us
cycles = (2700 x 40) / 1000 = 108 full square-wave cycles

The function blocks the calling task for approximately 40 ms. This is intentional in the factory context — the main loop is not time-critical and auditory feedback must be synchronous with user actions (button confirm, menu selection). This function must not be called from ISR context.


All pin assignments and tuning constants for this board are centralised in hw_config.h (board-specific). The table below summarises the logical constants consumed by this module.

ConstantDriverDescription
BUTTON_1_PINbuttons.cGPIO for physical button 1
BUTTON_2_PINbuttons.cGPIO for physical button 2
BUTTON_3_PINbuttons.cGPIO for physical button 3
ENCODER_A_PINencoder.cPCNT quadrature channel A GPIO
ENCODER_B_PINencoder.cPCNT quadrature channel B GPIO
TOUCH_I2C_PORT_IDXtouch.cI2C peripheral port number
TOUCH_I2C_SDA_PINtouch.cI2C SDA GPIO
TOUCH_I2C_SCL_PINtouch.cI2C SCL GPIO
TOUCH_I2C_FREQ_HZtouch.cI2C clock speed
TOUCH_I2C_ADDR_LISTtouch.cFT5x06 I2C address(es)
TOUCH_IRQ_PINtouch.cTouch interrupt GPIO (optional, polled not used)
TOUCH_RST_PINtouch.cGPIO_NUM_NC — shared with TFT_RST_PIN
TOUCH_SWAP_XY_FLAGtouch.cAxis swap flag
TOUCH_MIRROR_X_FLAGtouch.cX-axis mirror flag
TOUCH_MIRROR_Y_FLAGtouch.cY-axis mirror flag
TOUCH_X_MAXtouch.cFixed raw X full-scale (not auto-detected)
TOUCH_Y_MAXtouch.cFixed raw Y full-scale (not auto-detected)
SCREEN_WIDTHtouch.cTarget display pixel width
SCREEN_HEIGHTtouch.cTarget display pixel height
BUZZER_PINbuzzer.cGPIO driving the passive buzzer

Any pin defined as GPIO_NUM_NC causes the corresponding driver to silently skip initialisation. This makes the same source files compilable and functional across board variants that may omit certain peripherals.


ESP-IDF

Shared Hardware Drivers

Input Subsystem (this module)

buttons.c

encoder.c

touch.c / touch.h

buzzer.c

touch_ft5x06

hardware/common/drivers/touch_ft5x06/

bus_i2c

hardware/common/drivers/bus_i2c/

driver/gpio

driver/pulse_cnt

esp_rom_sys

hw_config.h

(board pin map)

Factory App Main

(factory_app_main)

The display driver must run before touch_init() because the FT5x06 shares the TFT reset line.

ESP-IDF driversbuzzer.ctouch.cencoder.cbuttons.cgfx / display drivermain.c (app_main)ESP-IDF driversbuzzer.ctouch.cencoder.cbuttons.cgfx / display drivermain.c (app_main)Board power-on / app_main()Display reset pulse also brings FT5x06 out of resetAll inputs ready, enter factory menu loopst7796_i80_init()buttons_init()gpio_config(BTN_1..3, INPUT + PULLUP)encoder_init()pcnt_new_unit() + 2x pcnt_new_channel()pcnt_unit_start()touch_init()bus_i2c_init(port, SDA, SCL)touch_ft5x06_configure(config)buzzer_init()gpio_config(BUZZER_PIN, OUTPUT) buzzer_beep_short()touch_read()encoder_read()button_is_pressed()Main Loop (~20 ms)buzzer_beep_short()touch_read()encoder_read()button_is_pressed()Main Loop (~20 ms)Blocks ~40 ms (bit-bang busy wait)alt[User action confirmed (e.g. menu select)]loop[Every ~20 ms]scan BTN_1, BTN_2, BTN_3pressed / not pressedget clicks since last pollint (positive=CW, negative=CCW, 0=no change)read touch statetouch_point_t {pressed, x, y}buzzer_beep_short()

6.1 Button Detection and Debounce (button_wait_press)

Section titled “6.1 Button Detection and Debounce (button_wait_press)”

Yes

No

No

Yes

No - glitch

Yes

Enter button_wait_press

Scan BTN_1 to BTN_3

every 20 ms

elapsed >= timeout_ms

AND timeout_ms != 0?

Return BTN_NONE

GPIO level = 0

(button pressed)?

vTaskDelay(20 ms)

elapsed += 20

vTaskDelay(50 ms)

Still pressed?

Poll until GPIO = 1

(button released)

vTaskDelay(50 ms)

Return button_id_t

No

Yes

No

Yes

encoder_read called

s_initialized?

Return 0

pcnt_unit_get_count()

count = raw PCNT value

clicks = count / PULSES_PER_DETENT

(integer division, remainder discarded)

clicks != 0?

Return 0

pcnt_unit_clear_count()

(avoids accumulation drift)

Return clicks

No

Yes

No

Yes

touch_read called

s_initialized?

Return pressed=false, x=-1, y=-1

touch_ft5x06_read()

raw x, y, is_pressed

is_pressed?

Return pressed=false, x=-1, y=-1

x = raw_x x SCREEN_WIDTH / x_max

y = raw_y x SCREEN_HEIGHT / y_max

Return touch_point_t


ConstraintDetail
No dynamic allocationAll state is held in static variables (s_pcnt_unit, s_initialized). No malloc anywhere in this module.
No ISRs or queuesButtons and touch are purely polled by the caller. The encoder uses hardware PCNT counting (hardware accumulates the count); the result is polled, not pushed via callback.
Blocking beepbuzzer_beep_short() uses esp_rom_delay_us busy-wait and blocks the caller for ~40 ms. Must not be called from ISR context. Acceptable in factory menu context where the loop is not time-critical.
GPIO_NUM_NC guard in every driverEvery init function checks pin validity at runtime. Invalid pins produce a zero bit-mask or an early return. The build remains valid for board variants that omit any peripheral.
Touch reset shared with displayTOUCH_RST_PIN = GPIO_NUM_NC. The FT5x06 must be initialized after the display driver has issued its reset pulse. Violating this ordering leaves the touch IC in reset and touch_ft5x06_configure will fail.
Fixed touch scaleAuto-detection of x_max/y_max is disabled. If the IC reports a range different from what hw_config.h specifies, the coordinate mapping will be wrong. Update hw_config.h manually if the IC changes.
PCNT counter cleared on every pollSub-detent pulses are discarded on each encoder_read() call. At polling intervals at or below 100 ms and 4 pulses per detent this is undetectable by the user.
Braces on all conditionalsPer project rules: FACTORY_LOGD macros can compile to empty statements; every if/else/for/while uses {} to remain correct when macro bodies disappear.

This input subsystem is an instance of a pattern repeated across every board that has a factory application. Sibling implementations follow the same public API and file structure, varying only the hardware details:

BoardTouch ICDisplay busNotable difference vs. this board
esp32s3_zx3d50ce02s_usrc_4832 (this)FT5x06 (I2C)Intel 8080 (ST7796)RST shared with TFT; fixed x/y max
esp32s3_hmi43v3FT5x06 (I2C)Intel 8080 (RM68120)Separate RST pin
esp32s3_bzm_tft35_gt911GT911 (I2C)SPI (ST7796)Different touch driver
esp32s3_8048s070cNoneRGB parallelNo touch in factory app
pibot_pendant_v1_0XPT2046 (SPI)SPI (ILI9341)Resistive touch with calibration

The public API (buttons_init, button_is_pressed, button_wait_press, encoder_init, encoder_read, touch_init, touch_read, touch_point_t, buzzer_init, buzzer_beep_short) is identical across all boards, allowing main.c to compile unchanged for every target.


ModuleRelationship
esp32s3_zx3d50ce02s_usrc_4832_factory_appParent: complete factory application for this board
esp32s3_zx3d50ce02s_usrc_4832_factory_app_mainDirect caller: polls all four input APIs from the factory menu loop
esp32s3_zx3d50ce02s_usrc_4832_factory_app_esp32s3_zx3d50ce02s_usrc_4832_factory_app_displaySibling display module: must be initialised first — its reset pulse wakes the FT5x06 touch IC
touch_driversShared touch_ft5x06 driver component used by touch.c
bus_driversShared bus_i2c component used to initialise the I2C bus for the FT5x06
physical_inputShared phy_encoder and phy_buttons driver components used by the main-firmware BSP layer
esp32s3_zx3d50ce02s_usrc_4832_bspBSP for the main firmware on this board; uses the same touch_ft5x06 driver and PCNT encoder pattern
buzzerMain-firmware sound module (esp3d_sound.h); the factory app uses this simpler standalone bit-bang implementation instead