Skip to content

BSP Physical Inputs

The bsp_physical_inputs module provides low-level drivers for the physical input devices of the PiBot CNC Pendant (pibot_pendant_v1_0). It abstracts four distinct hardware peripherals — push buttons, a rotary encoder, an analog potentiometer, and a 4-position rotary switch — into a consistent configure/read API consumed by the BSP layer.

These drivers sit at the bottom of the input stack: they own the hardware peripheral lifecycle (GPIO, PCNT, ADC) and expose simple read functions. The BSP board-init callbacks adapt those reads into LVGL input-device events and route them to the active screen as typed control_event_t structs.


ESP32 Hardware Peripherals

bsp_physical_inputs - Physical Input Drivers

BSP Layer - pibot_pendant_v1_0

Application Layer

LV_EVENT_PRESSED/KEY

LV_EVENT_SWITCH_PRESSED/RELEASED

LV_EVENT_POTENTIOMETER_CHANGED

phy_buttons_read()

phy_encoder_read()

phy_potentiometer_read()

phy_switch_read()

CNC Screens

(status, jog, macros…)

board_init.c

button_read_cb / encoder_read_cb

potentiometer_read_cb / switch_read_cb

control_event_t

+ custom LVGL events

control_events_init()

phy_buttons

GPIO debounced read

phy_encoder

PCNT quadrature decode

phy_potentiometer

ADC oneshot + filter

phy_switch

GPIO position decode

GPIO

PCNT Unit

ADC Oneshot

Board scope: These drivers are used exclusively by pibot_pendant_v1_0. Touchscreen-only boards (e.g. esp32s3_8048s043c, esp32_2432s028r) do not instantiate any phy_* driver. See bsp_board_initialization.md for the full board-init context and bsp_bsp_control_events.md for the event routing layer.


bsp_physical_inputs

phy_switch

phy_switch_config.h

phy_switch_config_t

phy_switch.h

configure / read / get_state

phy_potentiometer

phy_potentiometer_config.h

phy_potentiometer_config_t

phy_potentiometer.h

configure / read

phy_encoder

phy_encoder_config.h

phy_encoder_config_t

phy_encoder.h

configure / read / deinit

phy_encoder.c

PCNT + ISR queue

phy_buttons

phy_buttons_config.h

phy_buttons_config_t

phy_buttons.h

configure / read

Each sub-driver follows the same pattern:

FileRole
phy_<device>_config.hConfiguration struct (pins, timing, ADC params)
phy_<device>.hPublic API declarations
phy_<device>.cImplementation; holds static hardware state

Files: hardware/common/drivers/phy_buttons/

typedef struct {
gpio_num_t button_pins[3]; // GPIO pins for the 3 buttons
bool pullups_enabled; // Enable internal pull-up resistors
uint32_t debounce_ms; // Debounce window (milliseconds)
bool active_low; // true → pressed = GPIO low
} phy_buttons_config_t;
FunctionDescription
phy_buttons_configure(config)Initialises GPIO pins per config
phy_buttons_read(bool states[3])Reads all 3 buttons with debounce; sets states[i] = true when pressed
  • All three buttons are read in a single call; the BSP processes pressed and released transitions in separate passes to avoid missing multi-button events.
  • active_low = true is the hardware default on the pendant (buttons pull GPIO to GND when pressed, pull-ups enabled).

Files: hardware/common/drivers/phy_encoder/

typedef struct {
gpio_num_t pin_a; // Phase A GPIO
gpio_num_t pin_b; // Phase B GPIO
bool pullups_enabled; // Enable internal pull-ups
uint32_t debounce_us; // (reserved — PCNT glitch filter used instead)
uint32_t min_step_interval_us; // (reserved — adaptive throttle in BSP)
uint32_t steps_per_rev; // Pulses per full revolution (informational)
int32_t pcnt_high_limit; // PCNT counter upper wrap limit
int32_t pcnt_low_limit; // PCNT counter lower wrap limit
uint32_t pcnt_glitch_ns; // Glitch filter threshold (nanoseconds)
} phy_encoder_config_t;
FunctionDescription
phy_encoder_configure(config)Creates PCNT unit, two quadrature channels, glitch filter, watch-point callbacks, and starts counting
phy_encoder_read(int32_t *steps)Returns delta clicks since the last call; clears the PCNT counter
phy_encoder_get_total_steps(int32_t *total)Returns absolute accumulated click count since init
phy_encoder_reset_steps()Zeroes the accumulated counter and clears the PCNT unit
phy_encoder_get_config(config)Copies active config into caller-supplied struct
phy_encoder_deinit()Stops, disables, and deletes the PCNT unit; frees the event queue
BSP"phy_encoder_read()""FreeRTOS Queue""encoder_pcnt_on_reach (ISR)""PCNT Unit (ESP-IDF)""Encoder Pins (A, B)"BSP"phy_encoder_read()""FreeRTOS Queue""encoder_pcnt_on_reach (ISR)""PCNT Unit (ESP-IDF)""Encoder Pins (A, B)"Edge transitions on A/BQuadrature decode (±1 per click, 4X mode)Watch point reached (high/low limit)xQueueSendFromISR(watch_point_value)pcnt_unit_get_count()Drain watch events → reset if limit hitaccumulated += raw_countclicks = accumulated/4 − last_clickspcnt_unit_clear_count()delta clicks
  • 4X mode — both edges of A and B are counted, giving 4 pulses per mechanical detent.
  • Limit watch-points (pcnt_high_limit, pcnt_low_limit) fire an ISR when the counter wraps; the ISR enqueues the event so phy_encoder_read() can reset the accumulator on the next poll.
  • Glitch filter — a 500 ns hardware filter (hardcoded, overrides pcnt_glitch_ns) suppresses contact bounce at the PCNT level.
  • Interrupt priority 1 is used to keep encoder IRQ latency low without conflicting with higher-priority peripherals.

phy_potentiometer — Analog Potentiometer

Section titled “phy_potentiometer — Analog Potentiometer”

Files: hardware/common/drivers/phy_potentiometer/

typedef struct {
gpio_num_t pin; // GPIO / ADC input pin
adc_channel_t channel; // ESP-IDF ADC channel
adc_bitwidth_t width; // ADC resolution (e.g. ADC_BITWIDTH_12)
adc_atten_t atten; // Input attenuation (e.g. ADC_ATTEN_DB_12)
bool filter_enabled; // Enable multi-sample averaging
uint32_t filter_samples; // Number of samples to average
} phy_potentiometer_config_t;
FunctionDescription
phy_potentiometer_configure(config)Initialises ADC oneshot handle with the given channel and attenuation
phy_potentiometer_read(uint32_t *value)Returns raw ADC reading (0–4095 at 12-bit); applies multi-sample averaging when filter_enabled = true

The BSP potentiometer_read_cb maps the 0–4095 raw value to 0–100 and applies adaptive noise thresholds:

ConditionThreshold applied
After POT_INACTIVITY_THRESHOLD_MS of no changePOT_WAKE_THRESHOLD_MAPPED (high — prevents ADC-noise wake-up)
Direction reversal detected (active use)1 (ultra-sensitive)
Descending direction (active use)2
Ascending direction (active use)3

When a threshold is exceeded, the BSP fires LV_EVENT_POTENTIOMETER_CHANGED with control_event_t.steps set to the mapped 0–100 value.

The potentiometer is used as a view selector on the CNC status screen (status_screen.cpp) to cycle between position / override / planner data views. See cnc_common_screens.md for screen-level usage.


Files: hardware/common/drivers/phy_switch/

typedef struct {
gpio_num_t pins[3]; // 3 GPIO pins encoding 4 positions (binary)
bool pullups_enabled; // Enable internal pull-up resistors
uint32_t debounce_ms; // Debounce window (milliseconds)
} phy_switch_config_t;
FunctionDescription
phy_switch_configure(config)Initialises GPIO pins per config
phy_switch_read(bool *states)Decodes current position; fills states[4] as one-hot (only states[pos] = true); returns ESP_ERR_INVALID_RESPONSE for illegal pin combinations (transition noise)
phy_switch_get_state(uint32_t *key_code)Returns the current position index (0–3)
  • 3 GPIO pins → 4 positions using binary encoding. Invalid combinations (not a valid 3-bit pattern for positions 0–3) return ESP_ERR_INVALID_RESPONSE.
  • The BSP ignores invalid returns without calling activity_process_event(), preventing spurious screen wake-ups during position transitions.
  • Switch events drive axis/step-size selection in the jog screen and macro screens. See cnc_common_screens.md.

Active LVGL Screen

LVGL Event Dispatch

BSP board_init - LVGL indev callbacks

bsp_physical_inputs

Hardware

wake-up check

wake-up check

wake-up check

valid state only

Push Buttons

GPIO x3

Encoder

GPIO A/B → PCNT

Potentiometer

GPIO → ADC

Rotary Switch

GPIO x3

phy_buttons_read()

phy_encoder_read()

phy_potentiometer_read()

phy_switch_read()

button_read_cb()

encoder_read_cb()

potentiometer_read_cb()

switch_read_cb()

activity_process_event()

LV_EVENT_PRESSED / RELEASED

(control_family: BUTTONS)

LV_EVENT_KEY

(control_family: ENCODER, steps: ±1)

LV_EVENT_SWITCH_PRESSED / RELEASED

(control_family: SWITCH, btn_id: 0-3)

LV_EVENT_POTENTIOMETER_CHANGED

(control_family: POTENTIOMETER, steps: 0-100)

on_button_event / switch_event_cb

encoder_event_handler / potentiometer_event_cb


All four drivers report through the shared control_event_t type defined in the BSP:

typedef struct {
lv_indev_t *indev; // LVGL input-device handle (set by BSP callback)
uint32_t btn_id; // Button index (0–2) or switch position (0–3)
lv_indev_type_t type; // LV_INDEV_TYPE_BUTTON / LV_INDEV_TYPE_ENCODER / LV_INDEV_TYPE_POINTER
control_family_t family_id; // CONTROL_FAMILY_BUTTONS / ENCODER / SWITCH / POTENTIOMETER
int32_t steps; // Encoder: ±1 per normalised click; Potentiometer: mapped 0–100
uint32_t press_duration; // Buttons: ms held before release; others: 0
} control_event_t;

Custom LVGL event codes are registered at startup by control_events_init():

LV_EVENT_SWITCH_PRESSED = lv_event_register_id();
LV_EVENT_SWITCH_RELEASED = lv_event_register_id();
LV_EVENT_POTENTIOMETER_CHANGED = lv_event_register_id();

Standard LVGL events (LV_EVENT_PRESSED, LV_EVENT_RELEASED, LV_EVENT_KEY) are reused for buttons and the encoder respectively.

See bsp_bsp_control_events.md for the complete control event taxonomy and registration lifecycle.


The BSP encoder callback throttles output rate based on how fast the user is turning, preventing LVGL queue saturation while keeping fast-turn response smooth:

yes

no

yes

no

yes

no

yes

no

phy_encoder_read() → clicks ≠ 0

Measure time_since_last (ms)

≥ SLOW threshold?

≥ NORMAL threshold?

≥ FAST threshold?

min_interval = 80 ms

min_interval = 40 ms

min_interval = 20 ms

min_interval = 10 ms

time_since_last ≥ min_interval?

Send up to 5 LV_EVENT_KEY events

(step = ±1 each, direction = LV_KEY_LEFT/RIGHT)

Skip (data->state = RELEASED)

  • Maximum 5 events per callback call prevents cascading LVGL queue buildup on fast turns.
  • The ENCODER_INVERT_ROTATION compile-time flag negates clicks before direction mapping, allowing physical mounting in either orientation without driver changes.

All four BSP callbacks participate in the activity manager to support screen dim/sleep functionality:

"Active LVGL Screen""activity_process_event()""BSP read callback""phy_* driver""Active LVGL Screen""activity_process_event()""BSP read callback""phy_* driver"Prevents accidental jog/action on wakealt[System was sleeping][System was already awake]read hardware statestate changedactivity_process_event()returns false (wake-up event)consume input - do NOT send to UIreturns truelv_obj_send_event(active_screen, …, &control_event)

Key difference: phy_switch_read() returns ESP_ERR_INVALID_RESPONSE for invalid GPIO combinations (mechanical transition noise). The BSP switch_read_cb skips activity_process_event() entirely in this case, preventing accidental wake-ups from switch bouncing between positions.


phy_encoder_configure()

pcnt_unit_start()

pcnt_unit_get_count() + clear [per read]

Watch point fired → ISR → queue

phy_encoder_read() drains queue → resets accumulator

phy_encoder_deinit()

Unconfigured

Configured

PCNT unit created

Two quadrature channels installed

Glitch filter set (500 ns)

Watch points: high + low limits

ISR callback registered

Queue created (depth 10)

Counter cleared and started

Counting

LimitHit

pcnt_unit_stop()

pcnt_unit_disable()

pcnt_del_unit()

vQueueDelete()

State variables zeroed


BoardButtonsEncoderPotentiometerSwitchTouch
pibot_pendant_v1_0✅✅✅✅✅
esp32_2432s028r————✅
esp32_3248s035c/r————✅
esp32s3_4827s043c————✅
esp32s3_8048s043c/050c/070c————✅
esp32s3_bzm_tft35_gt911————✅
esp32s3_hmi43v3————✅
esp32s3_zx3d50ce02s_usrc_4832————✅
dlc32_max_lcd————✅
fysetc_wifi_pro—————

Touch controller drivers for all boards are covered in bsp_touch_controllers.md.


DriverESP-IDF PeripheralKey API
phy_buttonsGPIOgpio_config(), gpio_get_level()
phy_encoderPCNT (Pulse Counter Unit)pcnt_new_unit(), pcnt_new_channel(), pcnt_unit_register_event_callbacks(), pcnt_unit_get_count()
phy_potentiometerADC Oneshotadc_oneshot_new_unit(), adc_oneshot_config_channel(), adc_oneshot_read()
phy_switchGPIOgpio_config(), gpio_get_level()

Memory note: All driver state is held in static variables. No heap allocation occurs after phy_encoder_configure() (which allocates a FreeRTOS queue of depth 10 for PCNT events). This satisfies the project’s constraint that critical paths avoid runtime heap allocation. See docs/guides/esp32_memory_constraints.md for the full allocation policy.


ModuleRelationship
bsp_board_initialization.mdHosts the BSP board_init() that calls phy_*_configure() and registers LVGL indev callbacks
bsp_bsp_control_events.mdDefines control_event_t, control_family_t, and custom LVGL event IDs consumed by this module’s callers
bsp_touch_controllers.mdParallel input sub-system for capacitive/resistive touch; shares the same BSP board_init lifecycle
bsp_bus_drivers.mdI2C/SPI bus drivers used by touch controllers; no direct dependency from phy_* drivers
bsp_sensor_analog.mdGeneral-purpose ADC sensor driver; phy_potentiometer is a specialised instance of the same ADC oneshot pattern
cnc_common_screens.mdCNC UI screens that consume control_event_t events from buttons, encoder, potentiometer, and switch