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 GPIO6Each 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.