ESP32-S3 Baremetal Support

esp32s3_spi.c

SPI master on GP-SPI2 - clock, two data lines, and a chip select.

SPI is full duplex by nature: every clock edge shifts a bit out and a bit in at the same time, so a transfer of n bytes always moves n bytes in both directions whether or not you care about both. spi_transfer() is the honest form of that; spi_write() and spi_read() are the same call with one side thrown away.

spi_init(12, 11, 13, 10, 10000000, 0);   // SCK, MOSI, MISO, CS, 10 MHz
uint8_t cmd[] = { 0x9F }, id[3];
gpio_set_low(10);                  // if you drive CS yourself
spi_write(cmd, 1);
spi_read(id, 3);

One bus, one device. The controller has three chip-select outputs but this driver wires up only the first, because juggling several devices means changing clock and mode between them, and that belongs in whatever code knows what those devices are. For a second device, pass SPI_PIN_NONE for the chip select and drive the pins yourself with gpio_write().

Transfers of any length work, but the hardware moves 64 bytes at a time - that is the size of its data buffer, and this driver does not use DMA. Longer transfers are split, with the chip select held down across the seam so the device sees one unbroken transaction.

The bit clock comes from the 80 MHz PLL, so it does not shift when the CPU clock does. A request lands on the nearest rate at or below what you asked for, never above - overshooting a device's rated clock is the failure that shows up as rare corrupt bytes rather than as nothing working at all. The dividers reach down to 78125 Hz and no further, so a request below that gets 78125 Hz, the one case where the rate comes out higher than asked.

API

Declared in esp32s3_spi.h

Functions

void spi_init(uint32_t sck_pin, uint32_t mosi_pin, uint32_t miso_pin, uint32_t cs_pin, uint32_t hz, uint32_t mode);

Sets the bus up and puts it on four pins. The mode is the usual SPI 0-3, which is two independent choices written as one number: whether the clock idles low (0, 1) or high (2, 3), and whether data is sampled on the first edge of each bit (0, 2) or the second (1, 3). Mode 0 is what most devices want; the datasheet will say.

sck_pin
GPIO for the clock
mosi_pin
GPIO for data out, or SPI_PIN_NONE
miso_pin
GPIO for data in, or SPI_PIN_NONE
cs_pin
GPIO for chip select, or SPI_PIN_NONE to drive it yourself
hz
bit clock, e.g. 10000000
mode
0, 1, 2 or 3
returns
nothing
void spi_transfer(const void *tx, void *rx, uint32_t len);

Sends and receives at the same time, which is what the wires do anyway.

tx
bytes to send, or 0 to send zeros
rx
where to put what arrives, or 0 to discard it
len
how many bytes, any length
returns
nothing
void spi_write(const void *data, uint32_t len);

Sends bytes and discards what comes back.

data
the bytes to send
len
how many
returns
nothing
void spi_read(void *buf, uint32_t len);

Reads bytes, sending zeros to clock them out.

buf
where to put them
len
how many
returns
nothing
uint8_t spi_transfer_byte(uint8_t byte);

Sends one byte and returns the one that arrived in its place.

byte
the byte to send
returns
the byte received

Constants

#define SPI_PIN_NONE 0xFFFFFFFFu

Pass instead of a pin number to leave that signal unwired.

Source

board/esp32s3_spi.c
board/esp32s3_spi.c
188 lines
/*
 * SPI master on GP-SPI2, CPU-driven through the 64-byte data buffer.
 */

#include "esp32s3_spi.h"
#include "esp32s3_clock.h"
#include "esp32s3_gpio.h"
#include "esp32s3_regs.h"

// The controller's own clock, selected in spi_init(). Fixed at 80 MHz off the
// PLL, which is why the bit rate does not move when the CPU clock does.
#define SPI_SRC_HZ      80000000u

// W0..W15 hold one transfer, and this driver does not use DMA.
#define SPI_CHUNK_BYTES (SPI_BUF_WORDS * 4)

#define SPI_REG(off)    ESP32S3_REG(SPI2_BASE + (off))

static int spi_has_cs;      // whether the hardware drives chip select for us

// The bit clock is the source divided twice: a pre-divider of 1..16, then a
// counter of 2..64 that also shapes the high and low halves of each tick.
// Every combination is tried, and the fastest rate that does not exceed what
// was asked for wins - overshooting a device's rated clock is the one error
// here that shows up as rare corrupt bytes rather than as nothing working.
static void set_clock(uint32_t hz)
{
    if (hz >= SPI_SRC_HZ) {
        SPI_REG(SPI_CLOCK_OFF) = SPI_CLK_EQU_SYSCLK;    // bypass both dividers
        return;
    }

    uint32_t best_pre = 16, best_n = 64, best_hz = 0;

    for (uint32_t pre = 1; pre <= 16; pre++) {
        for (uint32_t n = 2; n <= 64; n++) {
            uint32_t f = SPI_SRC_HZ / (pre * n);
            if (f <= hz && f > best_hz) {
                best_hz  = f;
                best_pre = pre;
                best_n   = n;
            }
        }
    }

    // Low for the first half of the tick, high for the rest.
    SPI_REG(SPI_CLOCK_OFF) =
          ((best_n - 1) << SPI_CLKCNT_L_S)
        | ((best_n / 2 - 1) << SPI_CLKCNT_H_S)
        | ((best_n - 1) << SPI_CLKCNT_N_S)
        | ((best_pre - 1) << SPI_CLKDIV_PRE_S);
}

void spi_init(uint32_t sck_pin, uint32_t mosi_pin, uint32_t miso_pin,
              uint32_t cs_pin, uint32_t hz, uint32_t mode)
{
    periph_enable(SYSTEM_PERIP_CLK_EN0_REG, SYSTEM_PERIP_RST_EN0_REG, SYSTEM_SPI2_CLK_EN);

    SPI_REG(SPI_CLK_GATE_OFF) = SPI_CLK_GATE_EN | SPI_MST_CLK_ACTIVE | SPI_MST_CLK_SEL;
    SPI_REG(SPI_SLAVE_OFF)    = 0;                  // master
    SPI_REG(SPI_DMA_CONF_OFF) = 0;

    set_clock(hz);

    // Full duplex, and no command, address or dummy phase - just data. Those
    // phases exist for flash chips that want an opcode sent a particular way;
    // an ordinary device gets its opcode as the first data byte instead.
    uint32_t user = SPI_DOUTDIN | SPI_USR_MOSI | SPI_USR_MISO;
    uint32_t misc = SPI_CS0_DIS | SPI_CS1_DIS | SPI_CS2_DIS;

    // The two halves of the mode number. CK_IDLE_EDGE is the clock's resting
    // level; CK_OUT_EDGE picks which edge of each bit the data changes on,
    // and it is deliberately not a straight copy of the phase bit - modes 0
    // and 3 want one setting, modes 1 and 2 the other.
    if (mode == 2 || mode == 3) {
        misc |= SPI_CK_IDLE_EDGE;
    }
    if (mode == 1 || mode == 2) {
        user |= SPI_CK_OUT_EDGE;
    }

    SPI_REG(SPI_USER1_OFF) = 0;
    SPI_REG(SPI_USER2_OFF) = 0;

    spi_has_cs = (cs_pin != SPI_PIN_NONE);
    if (spi_has_cs) {
        misc &= ~SPI_CS0_DIS;
        gpio_route_out(cs_pin, SPI2_CS0_SIG);
    }

    SPI_REG(SPI_USER_OFF) = user;
    SPI_REG(SPI_MISC_OFF) = misc;

    gpio_route_out(sck_pin, SPI2_CLK_SIG);
    if (mosi_pin != SPI_PIN_NONE) {
        gpio_route_out(mosi_pin, SPI2_MOSI_SIG);
    }
    if (miso_pin != SPI_PIN_NONE) {
        gpio_route_in(miso_pin, SPI2_MISO_SIG, GPIO_FLOAT);
    }

    SPI_REG(SPI_CMD_OFF) = SPI_UPDATE;
    while (SPI_REG(SPI_CMD_OFF) & SPI_UPDATE) { }
}

// One transfer of up to 64 bytes. keep_cs holds the chip select down
// afterwards so a longer transfer reads as a single transaction to the device.
static void transfer_chunk(const uint8_t *tx, uint8_t *rx, uint32_t len, int keep_cs)
{
    // The data buffer is sixteen 32-bit registers, not a byte array, so the
    // bytes are packed in and unpacked out a word at a time. Unsent lanes of
    // the last word are don't-care; the bit length is what ends the transfer.
    for (uint32_t i = 0; i < len; i += 4) {
        uint32_t word = 0;
        for (uint32_t b = 0; b < 4 && i + b < len; b++) {
            word |= (uint32_t)(tx ? tx[i + b] : 0u) << (8 * b);
        }
        SPI_REG(SPI_W_OFF(i / 4)) = word;
    }

    SPI_REG(SPI_MS_DLEN_OFF) = len * 8 - 1;

    uint32_t misc = SPI_REG(SPI_MISC_OFF);
    if (keep_cs) {
        misc |= SPI_CS_KEEP_ACTIVE;
    } else {
        misc &= ~SPI_CS_KEEP_ACTIVE;
    }
    SPI_REG(SPI_MISC_OFF) = misc;

    // Most of these registers are read on the SPI clock rather than the CPU's,
    // so the writes above only take effect once they are handed across.
    SPI_REG(SPI_CMD_OFF) = SPI_UPDATE;
    while (SPI_REG(SPI_CMD_OFF) & SPI_UPDATE) { }

    SPI_REG(SPI_CMD_OFF) = SPI_USR;
    while (SPI_REG(SPI_CMD_OFF) & SPI_USR) { }

    if (rx) {
        for (uint32_t i = 0; i < len; i += 4) {
            uint32_t word = SPI_REG(SPI_W_OFF(i / 4));
            for (uint32_t b = 0; b < 4 && i + b < len; b++) {
                rx[i + b] = (uint8_t)(word >> (8 * b));
            }
        }
    }
}

void spi_transfer(const void *tx, void *rx, uint32_t len)
{
    const uint8_t *out = tx;
    uint8_t *in = rx;

    while (len > 0) {
        uint32_t chunk = len > SPI_CHUNK_BYTES ? SPI_CHUNK_BYTES : len;
        len -= chunk;

        // Hold the chip select down over every seam but the last one. With no
        // hardware chip select there is nothing to hold, and the caller is
        // driving the pin across the whole transfer anyway.
        transfer_chunk(out, in, chunk, spi_has_cs && len > 0);

        if (out) {
            out += chunk;
        }
        if (in) {
            in += chunk;
        }
    }
}

void spi_write(const void *data, uint32_t len)
{
    spi_transfer(data, 0, len);
}

void spi_read(void *buf, uint32_t len)
{
    spi_transfer(0, buf, len);
}

uint8_t spi_transfer_byte(uint8_t byte)
{
    uint8_t received = 0;

    spi_transfer(&byte, &received, 1);
    return received;
}
include/esp32s3_spi.h— the header, in full
include/esp32s3_spi.h
85 lines
/*
 * SPI master on GP-SPI2 - clock, two data lines, and a chip select.
 *
 * SPI is full duplex by nature: every clock edge shifts a bit out and a bit
 * in at the same time, so a transfer of n bytes always moves n bytes in both
 * directions whether or not you care about both. spi_transfer() is the honest
 * form of that; spi_write() and spi_read() are the same call with one side
 * thrown away.
 *
 *     spi_init(12, 11, 13, 10, 10000000, 0);   // SCK, MOSI, MISO, CS, 10 MHz
 *
 *     uint8_t cmd[] = { 0x9F }, id[3];
 *     gpio_set_low(10);                  // if you drive CS yourself
 *     spi_write(cmd, 1);
 *     spi_read(id, 3);
 *
 * One bus, one device. The controller has three chip-select outputs but this
 * driver wires up only the first, because juggling several devices means
 * changing clock and mode between them, and that belongs in whatever code
 * knows what those devices are. For a second device, pass SPI_PIN_NONE for
 * the chip select and drive the pins yourself with gpio_write().
 *
 * Transfers of any length work, but the hardware moves 64 bytes at a time -
 * that is the size of its data buffer, and this driver does not use DMA.
 * Longer transfers are split, with the chip select held down across the seam
 * so the device sees one unbroken transaction.
 *
 * The bit clock comes from the 80 MHz PLL, so it does not shift when the CPU
 * clock does. A request lands on the nearest rate at or below what you asked
 * for, never above - overshooting a device's rated clock is the failure that
 * shows up as rare corrupt bytes rather than as nothing working at all. The
 * dividers reach down to 78125 Hz and no further, so a request below that
 * gets 78125 Hz, the one case where the rate comes out higher than asked.
 */

#ifndef ESP32S3_SPI_H
#define ESP32S3_SPI_H

#include <stdint.h>

// Pass instead of a pin number to leave that signal unwired.
#define SPI_PIN_NONE    0xFFFFFFFFu

// Sets the bus up and puts it on four pins.
//
// The mode is the usual SPI 0-3, which is two independent choices written as
// one number: whether the clock idles low (0, 1) or high (2, 3), and whether
// data is sampled on the first edge of each bit (0, 2) or the second (1, 3).
// Mode 0 is what most devices want; the datasheet will say.
//
// - sck_pin: GPIO for the clock
// - mosi_pin: GPIO for data out, or SPI_PIN_NONE
// - miso_pin: GPIO for data in, or SPI_PIN_NONE
// - cs_pin: GPIO for chip select, or SPI_PIN_NONE to drive it yourself
// - hz: bit clock, e.g. 10000000
// - mode: 0, 1, 2 or 3
// returns: nothing
void spi_init(uint32_t sck_pin, uint32_t mosi_pin, uint32_t miso_pin,
              uint32_t cs_pin, uint32_t hz, uint32_t mode);

// Sends and receives at the same time, which is what the wires do anyway.
// - tx: bytes to send, or 0 to send zeros
// - rx: where to put what arrives, or 0 to discard it
// - len: how many bytes, any length
// returns: nothing
void spi_transfer(const void *tx, void *rx, uint32_t len);

// Sends bytes and discards what comes back.
// - data: the bytes to send
// - len: how many
// returns: nothing
void spi_write(const void *data, uint32_t len);

// Reads bytes, sending zeros to clock them out.
// - buf: where to put them
// - len: how many
// returns: nothing
void spi_read(void *buf, uint32_t len);

// Sends one byte and returns the one that arrived in its place.
// - byte: the byte to send
// returns: the byte received
uint8_t spi_transfer_byte(uint8_t byte);

#endif // ESP32S3_SPI_H