ESP32-S3 Baremetal Support

esp32s3_gpio.c

Pins: plain digital I/O, and the GPIO matrix that peripherals reach pads through.

Two separate things live here.

The digital-output half is what a bit-banged LED needs: configure a pad and drive it. The set/clear helpers are single stores to a write-1-to-set register, so they are atomic against other pins and cheap enough to sit inside a timing-critical loop.

The matrix half is what every hardware peripheral needs. On this chip a peripheral is not wired to a fixed pad: UART, I2C, SPI and LEDC each emit a numbered *signal*, and a crossbar decides which pad carries it. That is why uart_init() and friends take pin numbers at all - almost any pin will do. gpio_route_out() and gpio_route_in() are the two sides of that crossbar, and the peripheral drivers are their only expected callers.

API

Declared in esp32s3_gpio.h

Types

typedef enum {
    GPIO_FLOAT = 0,
    GPIO_PULLUP,
    GPIO_PULLDOWN,
} gpio_pull_t;

What a pad holds itself at when nothing is driving it.

Functions

void gpio_config_output(uint32_t pin);

Makes a pad a digital output, ~20 mA drive, starting low.

pin
the GPIO number to configure
returns
nothing
void gpio_config_input(uint32_t pin, gpio_pull_t pull);

Makes a pad a digital input.

pin
the GPIO number to configure
pull
what holds the pin when nothing drives it
returns
nothing
void gpio_config_open_drain(uint32_t pin, gpio_pull_t pull);

Makes a pad open drain: it can pull low but never drives high, so several devices can share the line. This is what I2C needs on SDA and SCL.

pin
the GPIO number to configure
pull
GPIO_PULLUP to use the weak internal pull-up as the only pull-up
returns
nothing
int gpio_read(uint32_t pin);

Reads a pin's current level. The pad needs its input buffer on, which gpio_config_input() and gpio_config_open_drain() both do. Handles the pins above 31, which live in a second register.

pin
the GPIO number
returns
0 if low, 1 if high
void gpio_route_out(uint32_t pin, uint32_t signal);

Drives a pad from a peripheral's output signal, and configures the pad to suit. Push-pull: UART TX, SPI clock, MOSI and CS, an LEDC channel.

pin
the GPIO number that should carry it
signal
the peripheral signal number (e.g. SPI2_CLK_SIG)
returns
nothing
void gpio_route_in(uint32_t pin, uint32_t signal, gpio_pull_t pull);

Feeds a pad into a peripheral's input signal, and configures the pad as an input to suit. UART RX, SPI MISO.

pin
the GPIO number to read from
signal
the peripheral signal number (e.g. SPI2_MISO_SIG)
pull
what holds the pin when nothing drives it
returns
nothing
void gpio_route_open_drain(uint32_t pin, uint32_t signal, gpio_pull_t pull);

Wires a pad to a peripheral signal in both directions at once, open drain. This is the shape an I2C line has: one wire that either side may pull low and that a pull-up returns high, with the controller watching the result.

pin
the GPIO number
signal
the peripheral signal number (e.g. I2C_SDA_SIG(0))
pull
GPIO_PULLUP to lean on the weak internal pull-up
returns
nothing
static inline void gpio_set_high(uint32_t pin);

Drives a pin high. GPIO0..GPIO31 only: the pins above 31 are in a second register, and branching to pick one would cost cycles in the LED's bit loop.

pin
the GPIO number
returns
nothing
static inline void gpio_set_low(uint32_t pin);

Drives a pin low. GPIO0..GPIO31 only.

pin
the GPIO number
returns
nothing
static inline void gpio_write(uint32_t pin, int level);

Drives a pin to a level. GPIO0..GPIO31 only.

pin
the GPIO number
level
0 for low, anything else for high
returns
nothing

Source

board/esp32s3_gpio.c
board/esp32s3_gpio.c
122 lines
/*
 * Pins: plain digital I/O, and the GPIO matrix that peripherals reach pads
 * through.
 */

#include "esp32s3_gpio.h"

// The IO MUX word for one pad: which of the pad's own functions is selected,
// how hard it drives, and whether the input buffer and pulls are on.
static void io_mux_config(uint32_t pin, gpio_pull_t pull, int input_enable)
{
    uint32_t cfg = (IO_MUX_FUNC_GPIO << IO_MUX_MCU_SEL_S)   // pad <-> GPIO matrix
                 | (2u << IO_MUX_FUN_DRV_S);                // ~20 mA

    if (input_enable) {
        cfg |= 1u << IO_MUX_FUN_IE_S;
    }
    if (pull == GPIO_PULLUP) {
        cfg |= 1u << IO_MUX_FUN_PU_S;
    } else if (pull == GPIO_PULLDOWN) {
        cfg |= 1u << IO_MUX_FUN_PD_S;
    }

    ESP32S3_REG(IO_MUX_GPIO_REG(pin)) = cfg;
}

// Output enable lives in a write-1-to-set register pair, with a second pair
// for the pins above 31.
static void output_enable(uint32_t pin, int enable)
{
    if (pin < 32) {
        ESP32S3_REG(enable ? GPIO_ENABLE_W1TS_REG : GPIO_ENABLE_W1TC_REG) = 1u << pin;
    } else {
        ESP32S3_REG(enable ? GPIO_ENABLE1_W1TS_REG : GPIO_ENABLE1_W1TC_REG) = 1u << (pin - 32);
    }
}

// gpio_set_high() and gpio_set_low() are bank 0 only, for the sake of the
// LED's bit loop. Configuration is not on that path and can afford the check.
static void drive(uint32_t pin, int level)
{
    if (pin < 32) {
        ESP32S3_REG(level ? GPIO_OUT_W1TS_REG : GPIO_OUT_W1TC_REG) = 1u << pin;
    } else {
        ESP32S3_REG(level ? GPIO_OUT1_W1TS_REG : GPIO_OUT1_W1TC_REG) = 1u << (pin - 32);
    }
}

void gpio_config_output(uint32_t pin)
{
    io_mux_config(pin, GPIO_FLOAT, 0);

    // GPIO matrix: take the pad's level straight from GPIO_OUT_REG bit <pin>.
    ESP32S3_REG(GPIO_FUNC_OUT_SEL_CFG_REG(pin)) = GPIO_SIG_OUT_IDX;

    // Start low, then enable the output driver.
    gpio_set_low(pin);
    output_enable(pin, 1);
}

void gpio_config_input(uint32_t pin, gpio_pull_t pull)
{
    io_mux_config(pin, pull, 1);
    output_enable(pin, 0);
}

void gpio_config_open_drain(uint32_t pin, gpio_pull_t pull)
{
    io_mux_config(pin, pull, 1);
    ESP32S3_REG(GPIO_FUNC_OUT_SEL_CFG_REG(pin)) = GPIO_SIG_OUT_IDX;
    ESP32S3_REG(GPIO_PIN_REG(pin)) |= GPIO_PIN_PAD_DRIVER;

    // Start released rather than pulling the shared line down the moment the
    // pad is enabled - on an open-drain bus a high is the absence of a low.
    drive(pin, 1);
    output_enable(pin, 1);
}

int gpio_read(uint32_t pin)
{
    if (pin < 32) {
        return (int)((ESP32S3_REG(GPIO_IN_REG) >> pin) & 1u);
    }
    return (int)((ESP32S3_REG(GPIO_IN1_REG) >> (pin - 32)) & 1u);
}

void gpio_route_out(uint32_t pin, uint32_t signal)
{
    io_mux_config(pin, GPIO_FLOAT, 0);

    // The pad still needs its output driver enabled; what the signal decides
    // is the level, not whether the pad is an output at all.
    ESP32S3_REG(GPIO_FUNC_OUT_SEL_CFG_REG(pin)) = signal;
    output_enable(pin, 1);
}

void gpio_route_in(uint32_t pin, uint32_t signal, gpio_pull_t pull)
{
    io_mux_config(pin, pull, 1);
    output_enable(pin, 0);

    // Without GPIO_SIG_IN_SEL the field below means "hold this signal at a
    // constant level" rather than "read it off this pad".
    ESP32S3_REG(GPIO_FUNC_IN_SEL_CFG_REG(signal)) =
        GPIO_SIG_IN_SEL | (pin & GPIO_FUNC_IN_SEL_M);
}

void gpio_route_open_drain(uint32_t pin, uint32_t signal, gpio_pull_t pull)
{
    // Input buffer on: on an open-drain bus the line's real level is whatever
    // it settles at, and the controller has to be able to see that.
    io_mux_config(pin, pull, 1);

    // PAD_DRIVER turns the push-pull driver into an open-drain one: a low
    // still pulls the line down, a high simply lets go of it.
    ESP32S3_REG(GPIO_PIN_REG(pin)) |= GPIO_PIN_PAD_DRIVER;

    ESP32S3_REG(GPIO_FUNC_OUT_SEL_CFG_REG(pin)) = signal;
    ESP32S3_REG(GPIO_FUNC_IN_SEL_CFG_REG(signal)) =
        GPIO_SIG_IN_SEL | (pin & GPIO_FUNC_IN_SEL_M);
    output_enable(pin, 1);
}
include/esp32s3_gpio.hโ€” the header, in full
include/esp32s3_gpio.h
112 lines
/*
 * Pins: plain digital I/O, and the GPIO matrix that peripherals reach pads
 * through.
 *
 * Two separate things live here.
 *
 * The digital-output half is what a bit-banged LED needs: configure a pad and
 * drive it. The set/clear helpers are single stores to a write-1-to-set
 * register, so they are atomic against other pins and cheap enough to sit
 * inside a timing-critical loop.
 *
 * The matrix half is what every hardware peripheral needs. On this chip a
 * peripheral is not wired to a fixed pad: UART, I2C, SPI and LEDC each emit a
 * numbered *signal*, and a crossbar decides which pad carries it. That is why
 * uart_init() and friends take pin numbers at all - almost any pin will do.
 * gpio_route_out() and gpio_route_in() are the two sides of that crossbar, and
 * the peripheral drivers are their only expected callers.
 */

#ifndef ESP32S3_GPIO_H
#define ESP32S3_GPIO_H

#include <stdint.h>
#include "esp32s3_regs.h"

// What a pad holds itself at when nothing is driving it.
typedef enum {
    GPIO_FLOAT = 0,
    GPIO_PULLUP,
    GPIO_PULLDOWN,
} gpio_pull_t;

// Makes a pad a digital output, ~20 mA drive, starting low.
// - pin: the GPIO number to configure
// returns: nothing
void gpio_config_output(uint32_t pin);

// Makes a pad a digital input.
// - pin: the GPIO number to configure
// - pull: what holds the pin when nothing drives it
// returns: nothing
void gpio_config_input(uint32_t pin, gpio_pull_t pull);

// Makes a pad open drain: it can pull low but never drives high, so several
// devices can share the line. This is what I2C needs on SDA and SCL.
// - pin: the GPIO number to configure
// - pull: GPIO_PULLUP to use the weak internal pull-up as the only pull-up
// returns: nothing
void gpio_config_open_drain(uint32_t pin, gpio_pull_t pull);

// Reads a pin's current level. The pad needs its input buffer on, which
// gpio_config_input() and gpio_config_open_drain() both do. Handles the pins
// above 31, which live in a second register.
// - pin: the GPIO number
// returns: 0 if low, 1 if high
int gpio_read(uint32_t pin);

// Drives a pad from a peripheral's output signal, and configures the pad to
// suit. Push-pull: UART TX, SPI clock, MOSI and CS, an LEDC channel.
// - pin: the GPIO number that should carry it
// - signal: the peripheral signal number (e.g. SPI2_CLK_SIG)
// returns: nothing
void gpio_route_out(uint32_t pin, uint32_t signal);

// Feeds a pad into a peripheral's input signal, and configures the pad as an
// input to suit. UART RX, SPI MISO.
// - pin: the GPIO number to read from
// - signal: the peripheral signal number (e.g. SPI2_MISO_SIG)
// - pull: what holds the pin when nothing drives it
// returns: nothing
void gpio_route_in(uint32_t pin, uint32_t signal, gpio_pull_t pull);

// Wires a pad to a peripheral signal in both directions at once, open drain.
// This is the shape an I2C line has: one wire that either side may pull low
// and that a pull-up returns high, with the controller watching the result.
// - pin: the GPIO number
// - signal: the peripheral signal number (e.g. I2C_SDA_SIG(0))
// - pull: GPIO_PULLUP to lean on the weak internal pull-up
// returns: nothing
void gpio_route_open_drain(uint32_t pin, uint32_t signal, gpio_pull_t pull);

// Drives a pin high. GPIO0..GPIO31 only: the pins above 31 are in a second
// register, and branching to pick one would cost cycles in the LED's bit loop.
// - pin: the GPIO number
// returns: nothing
static inline void gpio_set_high(uint32_t pin)
{
    ESP32S3_REG(GPIO_OUT_W1TS_REG) = 1u << pin;
}

// Drives a pin low. GPIO0..GPIO31 only.
// - pin: the GPIO number
// returns: nothing
static inline void gpio_set_low(uint32_t pin)
{
    ESP32S3_REG(GPIO_OUT_W1TC_REG) = 1u << pin;
}

// Drives a pin to a level. GPIO0..GPIO31 only.
// - pin: the GPIO number
// - level: 0 for low, anything else for high
// returns: nothing
static inline void gpio_write(uint32_t pin, int level)
{
    if (level) {
        gpio_set_high(pin);
    } else {
        gpio_set_low(pin);
    }
}

#endif // ESP32S3_GPIO_H