Skip to content

Document the CPHA=0 inter-frame gap in spi_set_format() - #3285

Open
Circuit-Overtime wants to merge 1 commit into
raspberrypi:developfrom
Circuit-Overtime:spi-cpha0-throughput-note
Open

Circuit-Overtime wants to merge 1 commit into
raspberrypi:developfrom
Circuit-Overtime:spi-cpha0-throughput-note

Conversation

@Circuit-Overtime

Copy link
Copy Markdown

Hello developers, just a heads up

The spi.h documents how chip select behaves with CPHA=1 (the notes added after #3141), but not that CPHA=0 costs throughput. This adds a \note to spi_set_format().

With CPHA=0 the PL022 returns to idle between frames, even when the TX FIFO never empties. It pulses SSPFSSOUT high between words, as described in the PL022 documentation and discussed in #941. It also leaves 1.5 SCK periods between consecutive frames, so sustained throughput is data_bits / (data_bits + 1.5) of the SCK rate. With CPHA=1, frames go back to back.

These are the measurement summary with setup

Setup: an RP2350 (Pico 2) with clk_sys = clk_peri = 150 MHz, SPI0 TX only. DMA from RAM paced by the TX DREQ, so the CPU can't starve the FIFO. 64 KiB per run, timed until spi_is_busy() clears. Efficiency is the wire time (bits ÷ SCK) over the measured time.

SCK Frame Mode 0 Mode 1 Mode 2 Mode 3
9.375 MHz 8-bit 84.21% 100.00% 84.21% 100.00%
9.375 MHz 16-bit 91.43% 100.00% 91.43% 100.00%
18.75 MHz 8-bit 84.21% 100.00% 84.21% 100.00%
18.75 MHz 16-bit 91.43% 100.00% 91.43% 100.00%
25 MHz 8-bit 84.21% 100.00% 84.21% 100.00%
25 MHz 16-bit 91.43% 100.00% 91.43% 100.00%
37.5 MHz 8-bit 84.21% 99.99% 84.21% 99.99%
37.5 MHz 16-bit 91.42% 100.00% 91.43% 99.99%
75 MHz 8-bit 84.20% 99.99% 84.20% 99.99%
75 MHz 16-bit 91.43% 99.99% 91.42% 100.01%

The gap works out to exactly 1.50 SCK periods per frame at every clock: 8/9.5 = 84.21%, 16/17.5 = 91.43%. CPOL makes no difference.

I measured this on the RP2350 only. The RP2040 has the same PL022, so I'd expect the same result, but I haven't run it there.

In practice: ST7789 displays sample on the rising edge in both mode 0 and mode 3. Driving one in mode 3 cut the full-screen 320×240 transfer from 39.1 ms to 33.0 ms at 37.5 MHz, which was enough to keep 24 FPS video with room to spare.

Test program (Pico SDK, prints CSV over USB serial)

measure() is exactly what produced the table above. The original started each run when the host sent a byte; this version simply starts once USB is connected.

#include <stdio.h>
#include "hardware/clocks.h"
#include "hardware/dma.h"
#include "hardware/spi.h"
#include "pico/stdlib.h"

#define PIN_SCK 18
#define PIN_TX  19
#define BYTES   65536

static uint8_t buffer[BYTES] __attribute__((aligned(4)));

static void measure(int dma, uint clk_hz, spi_cpol_t cpol, spi_cpha_t cpha, uint bits) {
    spi_init(spi0, clk_hz);
    spi_set_format(spi0, bits, cpol, cpha, SPI_MSB_FIRST);
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_TX, GPIO_FUNC_SPI);
    const uint actual = spi_get_baudrate(spi0);

    dma_channel_config c = dma_channel_get_default_config(dma);
    channel_config_set_transfer_data_size(&c, bits > 8 ? DMA_SIZE_16 : DMA_SIZE_8);
    channel_config_set_read_increment(&c, true);
    channel_config_set_write_increment(&c, false);
    channel_config_set_dreq(&c, spi_get_dreq(spi0, true));
    const uint count = bits > 8 ? BYTES / 2 : BYTES;

    const uint64_t start = time_us_64();
    dma_channel_configure(dma, &c, &spi_get_hw(spi0)->dr, buffer, count, true);
    dma_channel_wait_for_finish_blocking(dma);
    while (spi_is_busy(spi0)) {
        tight_loop_contents();
    }
    const uint64_t elapsed = time_us_64() - start;

    const double wire_us = (double)count * bits * 1e6 / actual;
    printf("SPI,%u,%u,%u,%u,%u,%llu,%.1f,%.2f\n", actual, (uint)cpol, (uint)cpha, bits,
           BYTES, (unsigned long long)elapsed, wire_us, 100.0 * wire_us / (double)elapsed);
    spi_deinit(spi0);
}

int main(void) {
    stdio_init_all();
    for (uint i = 0; i < BYTES; ++i) {
        buffer[i] = (uint8_t)(i * 37u + 11u);
    }
    const int dma = dma_claim_unused_channel(true);
    static const uint clocks[] = {9375000, 18750000, 25000000, 37500000, 75000000};
    while (!stdio_usb_connected()) {
        sleep_ms(50);
    }
    sleep_ms(500);
    printf("SPI,clk_hz,cpol,cpha,bits,bytes,time_us,wire_us,efficiency_pct\n");
    for (uint k = 0; k < sizeof clocks / sizeof clocks[0]; ++k) {
        for (uint bits = 8; bits <= 16; bits += 8) {
            for (uint mode = 0; mode < 4; ++mode) {
                measure(dma, clocks[k], (spi_cpol_t)(mode >> 1), (spi_cpha_t)(mode & 1), bits);
            }
        }
    }
    for (;;) {
        tight_loop_contents();
    }
}

I have formatted this markdown using a linter so that it looks pretty xD :p

With CPHA=0 the PL022 pulses chip select high and idles for 1.5 SCK
periods between frames, even with a full TX FIFO, so sustained throughput
is data_bits / (data_bits + 1.5) of the SCK rate. Measured with DMA on an
RP2350: 84.21% (8-bit) and 91.43% (16-bit) at 9.4 to 75 MHz, against
100% with CPHA=1.
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Please do not submit against master, use develop instead

@Circuit-Overtime
Circuit-Overtime changed the base branch from master to develop November 4, 2026 18:27
@Circuit-Overtime

Copy link
Copy Markdown
Author

Retargeted to develop; thanks for the reminder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant