ESP32-S3 Baremetal Support

The four buses

esp32s3.h pulls in UART, I2C, SPI and PWM alongside the GPIO and timing. All four are polled — no interrupt handler, no buffering behind your back, no scheduler. A call returns when the hardware has finished.

uart_init(1, 5, 6, 115200);              /* port 1, TX 5, RX 6 */
uart_print(1, "AT\r\n");
int c = uart_read_byte(1);               /* -1 if nothing arrived */

i2c_init(0, 8, 9, 400000);               /* SDA 8, SCL 9, 400 kHz */
uint8_t who;
i2c_write_read(0, 0x68, (uint8_t[]){ 0x75 }, 1, &who, 1);

spi_init(12, 11, 13, 10, 10000000, 0);   /* SCK, MOSI, MISO, CS, 10 MHz, mode 0 */
spi_write(cmd, 1);
spi_read(id, 3);

pwm_init(0, 6, 1000, 10);                /* channel 0, GPIO6, 1 kHz, 10-bit */
pwm_set_duty(0, 512);                    /* half on */

Almost any pin works for any of them, because none of these peripherals is wired to a fixed pad. A peripheral emits a numbered signal and a crossbar — the GPIO matrix — decides which pad carries it. That is the whole reason the init calls take pin numbers. gpio_route_out() and gpio_route_in() in esp32s3_gpio.h are the two sides of that crossbar; the bus drivers are their only expected callers.

examples/ has a runnable program for each of these, three of which verify themselves with nothing but a jumper wire:

make gpio_button  flash monitor    # BOOT button, no wiring at all
make uart_echo    flash monitor    # jumper GPIO5 to GPIO6
make spi_loopback flash monitor    # jumper GPIO11 to GPIO13
make i2c_scan     flash monitor    # a device on GPIO8/GPIO9
make pwm_fade     flash monitor    # an LED on GPIO6

Each header carries the limits of its own driver. The ones worth knowing up front:

Bus Clocked from Limit worth knowing
UART 40 MHz crystal 8N1 only; 128-byte hardware FIFO, nothing buffered in RAM
I2C 40 MHz crystal 32 bytes per transaction, 7-bit addresses, 1 kHz–1 MHz
SPI 80 MHz PLL one device; split into 64-byte chunks with CS held across
PWM 40 MHz crystal 8 channels sharing 4 timers, so 4 distinct frequencies

None of them is clocked from anything the CPU clock feeds, so a baud rate or a servo pulse cannot drift because set_cpu_160mhz() ran. That was deliberate: the LED timing story further down is what happens when a peripheral's timing does depend on the CPU clock, and it is not a debugging session worth repeating four more times.