Skip to main content

GPIO

GPIO stands for General Purpose Input/Output. It is the simplest interface between a microcontroller and the outside world: a pin is either HIGH (3.3 V) or LOW (0 V).


Input vs. Output Mode

Every GPIO pin can be configured as either an input or an output:

  • Output mode — the firmware sets the pin HIGH or LOW. Use this to control the e-ink RST pin, the display CS line, or the MAX98357A shutdown pin.
  • Input mode — the firmware reads the pin state. Use this for buttons and the e-ink BUSY pin.

In ESP-IDF, you configure a pin with gpio_config_t:

gpio_config_t cfg = {
.pin_bit_mask = (1ULL << GPIO_NUM_8), // GPIO8 (BTN_SELECT)
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_NEGEDGE, // interrupt on falling edge (press)
};
gpio_config(&cfg);

Pull-Up Resistors

When a button is not pressed, what value should the GPIO read?

Without any external connection, a floating input pin will pick up electrical noise and read random values — the hardware equivalent of an uninitialized variable.

A pull-up resistor connects the pin to VCC (3.3 V) through a high-value resistor (typically 10 kΩ). This means:

  • When the button is open (not pressed): pin reads HIGH (3.3 V)
  • When the button is closed (pressed): pin connects to GND and reads LOW (0 V)

Pull-up resistor schematic — VCC through 10 kΩ to GPIO node, then a button to GND

The ESP32-S3 has built-in pull-up resistors on most GPIO pins (~45 kΩ internal), so you do not need external resistors in this project. Enable them in firmware with GPIO_PULLUP_ENABLE.

For a deeper explanation of how pull-up resistors work and how to choose values, see Essential Components: Pull-Up Resistors.


Interrupts

Polling a button pin in a loop wastes CPU cycles. Instead, configure a GPIO interrupt that fires a callback when the pin changes state.

Interrupt typeWhen it fires
GPIO_INTR_POSEDGERising edge: LOW → HIGH
GPIO_INTR_NEGEDGEFalling edge: HIGH → LOW (button press)
GPIO_INTR_ANYEDGEAny change
GPIO_INTR_LOW_LEVELWhile pin is LOW

For buttons connected to GND with pull-ups, use GPIO_INTR_NEGEDGE (fires on press).

ISR rules:

  • ISRs (interrupt service routines) run in a privileged, time-sensitive context
  • Do not call malloc, printf, or any FreeRTOS function that might block from an ISR
  • The correct pattern is to send to a FreeRTOS queue from the ISR and process in a task:
// ISR — fires on button press
static void IRAM_ATTR button_isr(void *arg) {
uint32_t gpio_num = (uint32_t)arg;
xQueueSendFromISR(button_event_queue, &gpio_num, NULL);
}

// Task — processes button events
static void button_task(void *arg) {
uint32_t gpio_num;
for (;;) {
if (xQueueReceive(button_event_queue, &gpio_num, portMAX_DELAY)) {
// handle button press
}
}
}

PWM Output

Some GPIO pins support PWM (Pulse-Width Modulation) output via the LEDC peripheral. This is how the e-ink front light brightness is controlled: instead of fully on or off, the pin pulses at high frequency with a variable duty cycle.

GPIO2 is used for the front light PWM. The firmware configures it with ledc_timer_config and ledc_channel_config — this is covered in the e-ink driver doc.


ESP32-S3 Specifics

  • Most GPIO pins are fully general purpose — you can use them as input, output, or for peripheral functions (SPI, I2C, I2S, LEDC).
  • Avoid strapping pins (GPIO0, GPIO45, GPIO46) — these are sampled at boot to determine boot mode and flash configuration. A button on GPIO0 that is held down during power-on will put the chip in download mode.
  • Avoid USB pins (GPIO19, GPIO20) on boards that use USB OTG.
  • GPIO43 and GPIO44 are UART0 TX/RX used for the serial console and flashing.

The pin table lists all pins used in this project and which ones to avoid.