Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 20 мин чтения
Открыть в интерактивном справочнике

18. Шина SPI

После I²C кажется отдыхом: проще устроен, работает на порядок быстрее, подтяжек не требует. Плата за это — больше проводов и по отдельной линии выбора на каждое устройство. Раздел разбирает режимы такта на уровне фронтов — с моделью, которая показывает, во что превращается байт при неверном режиме, — скорости F103 и F407, и три устройства, которые встречаются почти в каждом приборе: флеш-память, SD-карту и цепочку сдвиговых регистров.

Как это называется в документации

Оригинал Перевод
master / slave ведущий / ведомый
clock polarity (CPOL) полярность такта
clock phase (CPHA) фаза такта
slave select (NSS / CS) выбор ведомого
full duplex / half duplex полный / полудуплексный обмен
baud rate prescaler предделитель скорости
busy flag (BSY) признак незавершённого обмена
data frame format (DFF) разрядность посылки
MSB / LSB first старшим / младшим битом вперёд
setup / hold time время установки / удержания данных
JEDEC ID идентификатор производителя и микросхемы
daisy chain цепочка: выход одного — вход следующего
latch (RCLK) защёлка: перенос сдвигового регистра на выходы
overrun (OVR) приём поверх непрочитанного (переполнение приёма)

18.1 Четыре провода

Линия Назначение
SCK такт, формирует ведущий
MOSI данные от ведущего к ведомому
MISO данные от ведомого к ведущему
CS (NSS) выбор устройства, активный уровень низкий

Первые три общие для всех устройств на шине, а линия выбора — своя у каждого. Три микросхемы потребуют шесть выводов: три общих плюс три отдельных CS.

Три ведомых на SPI1: общие SCK, MOSI и MISO, у каждой микросхемы свой CS на обычном выводе GPIO, активный уровень низкий

Главное свойство, которое надо усвоить сразу: обмен всегда двусторонний. Внутри стоит кольцевой сдвиговый регистр — с каждым тактом бит уходит из ведущего и одновременно бит приходит от ведомого. В обычном двунаправленном режиме отдельной операции «только принять» нет. (Режим RXONLY есть, но в нём ведущий гонит такт без остановки, пока блок включён, и выключить его ровно после нужного байта — отдельная задача.)

Отсюда идиома, которая поначалу озадачивает:

uint8_t spi1_read(void)
{
    return spi1_xfer(0xFFu);
}

Такт формирует ведущий, поэтому чтобы ведомый что-то выдал, нужно что‑то отправить. Значение не важно — обычно шлют 0xFF или нули.

На F103 и F407 блок SPI устроен одинаково: те же биты CR1, те же флаги SR. Различаются выводы и тактирование: у F407 альтернативная функция выбирается номером в AFR — для SPI1 это 5.


18.2 Четыре режима: CPOL и CPHA

Четыре режима SPI: такт в покое, фронт чтения и фронт смены данных для байта 0x9F

Два бита определяют, как выглядит такт и когда защёлкиваются данные:

Режим CPOL CPHA Такт в покое Данные читаются
0 0 0 низкий по нарастающему фронту
1 0 1 низкий по спадающему
2 1 0 высокий по спадающему
3 1 1 высокий по нарастающему

CPHA = 0: первый бит выставлен ещё до первого фронта, читается по первому фронту, меняется по второму. CPHA = 1: бит выставляется по первому фронту, читается по второму. Ходовые — 0 и 3: в обоих данные читаются по нарастающему фронту, поэтому многие микросхемы понимают любой из них. Режимы 1 и 2 встречаются редко.

Что будет при неверном режиме — модель. Ведущий передаёт байт в своём режиме, ведомый читает в своём; на фронте, где ведущий меняет данные, ведомый успевает прочитать старое значение — так и есть у настоящих микросхем, это время удержания. Итог для байта 0x9F: ведущий в режиме 1, а ведомый в режиме 0 или 3 читает 0x4F вместо 0x9F — каждый бит опоздал на такт, первым прочитан ноль с линии. А ведущий в режиме 0 прочитан верно всеми четырьмя ведомыми: читают они по «своему» фронту или по фронту смены, данные в этот момент ещё прежние.

⚠️ Неверный режим не даёт ошибки и может не проявиться сразу. Обмен идёт, байты приходят — просто не те, или те, но только в одну сторону и только на низкой скорости. Типичный вид — сдвиг на один бит или устойчивый мусор, похожий на данные. Если микросхема не отвечает идентификатором, который обязана вернуть, первым делом проверьте режим.


18.3 Скорость

Задаётся предделителем от 2 до 256, то есть быстрее половины частоты шины не бывает. Сверху есть и второй предел — из описания камня: у F103 такт SPI не выше 18 МГц и у ведущего, и у ведомого.

Шина Максимум
SPI1 (F1) APB2, 72 МГц 18 МГц — предел из описания; делитель 2 дал бы 36 МГц
SPI2 (F1) APB1, 36 МГц 18 МГц
SPI1 (F4) APB2, 84 МГц 42 МГц
SPI2, SPI3 (F4) APB1, 42 МГц 21 МГц

Как и у UART, порты сидят на разных шинах, и один и тот же делитель даст разные скорости. На F407 выводам SPI на высокой скорости нужен и быстрый фронт: поле OSPEEDR — иначе при 21–42 МГц фронты заваливаются.

Практические ориентиры:


18.4 Выбор устройства: аппаратный или ручной

Линией CS можно управлять двумя способами, и почти всегда выбирают второй.

Аппаратный — контроллер сам дёргает вывод NSS. У F1 и F4 он опускается, когда ведущий начинает обмен, и поднимается, только когда блок выключат (SPE = 0). Поэтому он годится для одного ведомого, а конец транзакции приходится отмечать выключением SPI. У блоков нового образца есть ещё импульс между посылками (NSSP, 18.12) — он как раз снимает выбор между байтами и ломает многобайтные команды.

Программный — вы объявляете CS обычным выходом и управляете им сами. Так делают почти всегда: это предсказуемо и позволяет держать выбор на протяжении всей транзакции.

SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | (br << 3);  /* режим 0, 8 бит */

Оба бита обязательны: SSM переводит управление в программный режим, а SSI задаёт внутренний уровень. Забыли SSI — контроллер решит, что его самого выбрал другой ведущий, и уйдёт из режима ведущего.


18.5 🔧 Обмен блоком данных

void spi1_transfer_block(const uint8_t *tx, uint8_t *rx, uint32_t len)
{
    CS_LOW();
    for (uint32_t i = 0; i < len; i++) {
        uint8_t in = spi1_xfer(tx ? tx[i] : 0xFFu);
        if (rx) {
            rx[i] = in;
        }
    }
    /* RXNE говорит лишь о принятом байте; последний бит может ещё уходить. */
    while (SPI1->SR & SPI_SR_BSY) { }
    CS_HIGH();
}

⚠️ Перед снятием CS дождитесь сброса BSY. Флаг RXNE означает, что принятый байт лежит в регистре, но последние биты могут ещё идти по линии. Снимете выбор раньше — обрежете хвост посылки. Проявляется это как «иногда работает»: на низкой скорости успевает, на высокой нет.


18.6 SPI и DMA

Для потоковых задач — вывода на дисплей, чтения флеш‑памяти, приёма с АЦП — обмен байт за байтом в цикле съедает процессор целиком. Кадр дисплея 240×320 в 16 бит — это 150 килобайт, которые надо перегнать как можно быстрее.

Здесь DMA обязателен (раздел 9). Для двустороннего обмена нужны два потока — один на передачу, другой на приём. Если приём не нужен (вывод на дисплей), хватит одного потока на передачу: флаг переполнения приёма OVR взведётся, но передачу он не останавливает — теряются только принятые байты. Поэтому после передачи прочитайте DR и SR по разу — это сбрасывает RXNE и OVR, иначе следующий приём вернёт старый байт.


18.7 Что ещё умеет блок

16‑битные посылки. Бит DFF переключает разрядность. Для дисплеев, где цвет кодируется 16 битами, это вдвое сокращает число операций и нагрузку на шину. Менять DFF можно только при выключенном блоке:

SPI1->CR1 &= ~SPI_CR1_SPE;
SPI1->CR1 |= SPI_CR1_DFF;
SPI1->CR1 |= SPI_CR1_SPE;

Полудуплекс. Одна линия данных вместо двух, направление переключается программно. Экономит вывод, применяется в некоторых датчиках и дисплеях.

Режим ведомого. Микроконтроллер может сам быть ведомым — например, подчинённым модулем в большой системе. Тогда такт формирует внешний ведущий, а вы обязаны успевать за ним, что почти всегда означает DMA. Сколько на это времени и как строят протокол — раздел 17а.

Аппаратный расчёт CRC. Блок умеет считать контрольную сумму передаваемых данных сам — полезно в шумной среде.

I²S на F407. SPI2 и SPI3 умеют звуковой интерфейс I²S — это тот же блок в другом режиме: цифровой звук для кодеков и микрофонов.


18.8 Дисплей: время кадра

Кадр 240 × 320 точек по 16 бит — 1 228 800 бит. Предел по скорости SPI:

Это время без единой паузы между словами. Цикл с опросом TXE на такой скорости паузы оставляет — процессор не успевает класть слово в тот же такт, когда регистр освободился, — и реальная частота кадров ниже. Поэтому для дисплея — 16-битные посылки и DMA, а процессор тем временем готовит следующий кусок кадра. Если нужно быстрее — перерисовывать только изменившиеся области: у дисплейных контроллеров для этого есть команды окна.

Заливка цветом в 16-битном режиме, без приёма:

while (!(SPI1->SR & SPI_SR_TXE)) { }
SPI1->DR = color;

18.9 🔧 Идентификатор флеш-памяти JEDEC

Первое, что делают с новой SPI-флеш: читают идентификатор. Команда 0x9F, в ответ три байта — производитель (0xEF у Winbond), тип памяти и ёмкость в виде степени двойки. Если пришли 0xFF — нет ответа (MISO висит в единице), если 0x00 — вывод прижат к земле или микросхема не выбрана. Функция одна для F103 и F407:

void flash_jedec_id(uint8_t id[3])
{
    CS_LOW();
    spi1_xfer(0x9F);
    id[0] = spi1_xfer(0xFF);     /* производитель: 0xEF — Winbond */
    id[1] = spi1_xfer(0xFF);     /* тип памяти */
    id[2] = spi1_xfer(0xFF);     /* ёмкость: 2^id[2] байт */
    spi1_done();
    CS_HIGH();
}

Ответ на первый байт — 0x9F — пропускается: пока команда уходит, микросхема ещё ничего не знает и отдаёт мусор. Верный идентификатор — лучшая проверка режима, скорости и разводки сразу: запись и стирание (раздел 41) пробуют только после него.

Так чтение идентификатора выглядит на экране анализатора:

Чтение JEDEC ID на экране анализатора: CS, SCK 9 МГц, команда 0x9F и ответ 0xEF 0x40 0x18


18.10 SD-карта: кадр команды и CRC7

В режиме SPI SD-карта принимает команды кадрами по 6 байт: 01 и номер команды, 32 бита аргумента, CRC7 и стоповая единица. После перехода в режим SPI (CMD0 при опущенном CS) проверка CRC выключена — кроме CMD8: её карта проверяет всегда. Поэтому верный CRC нужен двум командам, CMD0 и CMD8. Считается он так:

uint8_t sd_crc7(const uint8_t *p, int n)
{
    uint8_t crc = 0;
    int i, b;
    for (i = 0; i < n; i++)
        for (b = 7; b >= 0; b--) {
            int in = ((p[i] >> b) & 1) ^ ((crc >> 6) & 1);
            crc = (uint8_t)((crc << 1) & 0x7F);
            if (in)
                crc ^= 0x09;
        }
    return (uint8_t)((crc << 1) | 1);
}

Проверка — два кадра, которые знает каждый, кто оживлял SD-карту: CMD0 — 0x40 00 00 00 00 95, CMD8 с аргументом 0x1AA — 0x48 00 00 01 AA 87. Перед CMD0 карта должна получить не меньше 74 тактов при снятом выборе — это десять посылок 0xFF, и всё это на скорости до 400 кГц (18.3); полный порядок запуска карты — в разделе 41.


18.11 🔧 Цепочка сдвиговых регистров 74HC595

Когда выводов не хватает, выходы наращивают сдвиговыми регистрами 74HC595: у каждого 8 выходов, выход последнего бита соединяют со входом следующего, и три микросхемы дают 24 выхода на трёх проводах — такт, данные и защёлку RCLK. Приёма нет, MISO не нужен. Сама микросхема и её входы — в разделе 22.1.

Порядок байтов обратный интуитивному: первый отправленный байт проталкивается дальше всех. Модель цепочки: отправлены C3, 5A, 81 — в ближнем к микроконтроллеру регистре 81, в дальнем C3. Поэтому байт для дальнего регистра уходит первым:

void hc595_write3(const uint8_t v[3])
{
    spi1_xfer(v[2]);
    spi1_xfer(v[1]);
    spi1_xfer(v[0]);
    spi1_done();                 /* последний бит должен дойти до регистра */
    RCLK_HIGH();                 /* фронт защёлки — все 24 выхода меняются разом */
    RCLK_LOW();
}

Цепочка из трёх 74HC595: выход QH' каждого регистра идёт на вход SER следующего, такт и защёлка общие; после посылок C3, 5A, 81 в ближнем регистре 81, в дальнем C3

Защёлка — главное удобство: пока байты едут по цепочке, выходы стоят как стояли, и все 24 меняются одним фронтом. Дёрнуть её до сброса BSY — значит защёлкнуть сдвинутое не до конца.


18.12 F0 и G0: разрядность в DS и байт в DR

Регистры SPI у F0 и G0 лежат там же, где у F1: CR1 0x00, CR2 0x04, SR 0x08, DR 0x0C. Режимы CPOL/CPHA, делитель BR, программный NSS через SSM/SSI — всё то же. Отличий два, и оба про разрядность.

Нет бита DFF. Разрядность посылки задаёт четырёхбитное поле DS в CR2: любое значение от 4 до 16 бит, число минус один — 0111 для 8 бит, 1111 для 16. Код с SPI_CR1_DFF на F0 не собирается: use of undeclared identifier 'SPI_CR1_DFF'.

    SPI1->CR2 = (7u << SPI_CR2_DS_Pos) | SPI_CR2_FRXTH;  /* 8 бит; RXNE — как только есть байт */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1;   /* /8 */
    SPI1->CR1 |= SPI_CR1_SPE;

У DR есть очередь. За регистром данных стоят очереди на четыре байта в каждую сторону; их заполнение видно в SR — поля FTLVL и FRLVL. Отсюда правило: при посылках до 8 бит к DR обращаются байтом. Обычная запись SPI1->DR = b — 16-битная, и блок кладёт в очередь два байта: на линию уйдут b и ноль, а приём собьётся на один байт. Бит FRXTH заставляет RXNE подниматься уже при одном принятом байте, а не при двух:

uint8_t spi1_xfer_f0(uint8_t out)
{
    *(volatile uint8_t *)&SPI1->DR = out;       /* именно байт: 16-битная запись кладёт в очередь два */
    while (!(SPI1->SR & SPI_SR_RXNE)) {
    }
    return *(volatile uint8_t *)&SPI1->DR;
}

В объектном файле это видно: обмен собирается в STRB и LDRB, а не в STRH/LDRH. Обмен блоком из 18.5 и все рецепты дальше переносятся на F0/G0 заменой одной этой функции.

Остальное — по таблице:

F1 F0 G0
такт SPI1 APB2ENR APB2ENR APBENR2
SCK/MISO/MOSI на PA5/PA6/PA7 AF по умолчанию AF0 AF0
PCLK для SPI1 при максимуме 72 МГц 48 МГц 64 МГц
SCK при делителе 8 9 МГц 6 МГц 8 МГц

Ещё у нового блока есть импульс NSS между посылками (NSSP в CR2) и аппаратный CRC любой длины (CRCL); ни то ни другое для дисплеев и флеш-памяти из этого раздела не нужно.


18.13 Грабли

Микросхема не отвечает. Проверьте по порядку: режим (CPOL/CPHA), не перепутаны ли MOSI и MISO, дошёл ли CS до нужного вывода, есть ли питание и общая земля. Идентификатор JEDEC (18.9) — быстрый тест.

Не выставлен SSI. Контроллер выходит из режима ведущего.

Данные обрезаются на высокой скорости. CS снят до сброса BSY.

Сдвиг на один бит. Почти всегда неверная фаза такта: в модели 0x9F превращается в 0x4F.

Мусор при нескольких устройствах. Два CS активны одновременно, либо неактивные устройства не отпускают MISO. Приличные микросхемы переводят MISO в высокоимпедансное состояние при снятом выборе, дешёвые — не всегда. SD-карта отпускает MISO только после такта при поднятом CS: после каждой транзакции с ней шлют один пустой байт 0xFF с уже поднятым CS.

Помехи на длинных проводах. SPI не имеет ни подтяжек, ни контроля ошибок: искажённый бит просто становится другим байтом. На макетке снижайте скорость.

Дисплей работает, флешка нет (или наоборот) на одной шине. Устройствам нужны разные режимы или разные скорости — перенастраивайте контроллер перед обращением к каждому.

После вывода на дисплей приём сломан. Передавали без чтения, взвёлся OVR. Прочитать DR и SR.

SD-карта не отвечает на CMD0. Неверный CRC, мало тактов перед командой или скорость выше 400 кГц.

На выходах 74HC595 байты перепутаны. Отправлены в прямом порядке — первым должен уходить байт дальнего регистра.


18.14 Итоги

Полные исходники

spi18.c — SPI1 ведущим на F103 и F407: идентификатор флеш-памяти, 74HC595, команда SD-карты, посылки дисплею.

/* Раздел 18 (обогащение): SPI1 ведущим на F103 и F407 — идентификатор флеш-памяти,
 * цепочка 74HC595, кадр команды SD-карты, 16-битные посылки для дисплея.
 * Собирается armclang с -include stm32f10x.h (STM32F10X_MD) или stm32f4xx.h (STM32F407xx). */
#include <stdint.h>

#if defined(STM32F10X_MD)
#define PCLK2_HZ 72000000u               /* SPI1 на APB2 */
#define PCLK1_HZ 36000000u               /* SPI2 на APB1 */
#else
#define PCLK2_HZ 84000000u
#define PCLK1_HZ 42000000u
#endif

#define CS_LOW()    (GPIOA->BSRR = 1u << (4 + 16))
#define CS_HIGH()   (GPIOA->BSRR = 1u << 4)
#define RCLK_LOW()  (GPIOA->BSRR = 1u << (3 + 16))
#define RCLK_HIGH() (GPIOA->BSRR = 1u << 3)

/* PA5 SCK, PA6 MISO, PA7 MOSI — SPI1; PA4 — выбор флеш-памяти, PA3 — защёлка 74HC595. */
void spi1_init(uint32_t br)
{
#if defined(STM32F10X_MD)
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;
    GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu)
               | (0x3u << 12)            /* PA3: выход 50 МГц */
               | (0x3u << 16)            /* PA4: выход 50 МГц */
               | (0xBu << 20)            /* PA5: альтернативная функция, push-pull */
               | (0x4u << 24)            /* PA6: вход без подтяжки */
               | (0xBu << 28);           /* PA7: альтернативная функция, push-pull */
#else
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    GPIOA->MODER = (GPIOA->MODER & ~0x0000FFC0u)
                 | (1u << 6) | (1u << 8)                 /* PA3, PA4 — выходы */
                 | (2u << 10) | (2u << 12) | (2u << 14); /* PA5–PA7 — альтернативная функция */
    GPIOA->OSPEEDR |= 0x0000FC00u;                       /* PA5–PA7 — высокая скорость фронтов */
    GPIOA->AFR[0] = (GPIOA->AFR[0] & 0x000FFFFFu) | (5u << 20) | (5u << 24) | (5u << 28);  /* AF5 = SPI1 */
#endif
    CS_HIGH();
    RCLK_LOW();
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | (br << 3);  /* режим 0, 8 бит */
    SPI1->CR1 |= SPI_CR1_SPE;
}

uint8_t spi1_xfer(uint8_t data)
{
    while (!(SPI1->SR & SPI_SR_TXE)) { }
    *(volatile uint8_t *)&SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE)) { }
    return (uint8_t)SPI1->DR;
}

static void spi1_done(void)
{
    while (SPI1->SR & SPI_SR_BSY) { }
}

/* Идентификатор JEDEC: команда 0x9F, в ответ производитель, тип и ёмкость. */
void flash_jedec_id(uint8_t id[3])
{
    CS_LOW();
    spi1_xfer(0x9F);
    id[0] = spi1_xfer(0xFF);     /* производитель: 0xEF — Winbond */
    id[1] = spi1_xfer(0xFF);     /* тип памяти */
    id[2] = spi1_xfer(0xFF);     /* ёмкость: 2^id[2] байт */
    spi1_done();
    CS_HIGH();
}

/* Три 74HC595 в цепочке: v[0] — ближний к микроконтроллеру, поэтому первым уходит v[2]. */
void hc595_write3(const uint8_t v[3])
{
    spi1_xfer(v[2]);
    spi1_xfer(v[1]);
    spi1_xfer(v[0]);
    spi1_done();                 /* последний бит должен дойти до регистра */
    RCLK_HIGH();                 /* фронт защёлки — все 24 выхода меняются разом */
    RCLK_LOW();
}

/* CRC7 команды SD: многочлен x^7 + x^3 + 1, в конце сдвиг и единица стопового бита. */
uint8_t sd_crc7(const uint8_t *p, int n)
{
    uint8_t crc = 0;
    int i, b;
    for (i = 0; i < n; i++)
        for (b = 7; b >= 0; b--) {
            int in = ((p[i] >> b) & 1) ^ ((crc >> 6) & 1);
            crc = (uint8_t)((crc << 1) & 0x7F);
            if (in)
                crc ^= 0x09;
        }
    return (uint8_t)((crc << 1) | 1);
}

/* Кадр команды SD: 01 + номер, 32 бита аргумента, CRC7 и стоповая единица — 6 байт. */
void sd_frame(uint8_t cmd, uint32_t arg, uint8_t out[6])
{
    out[0] = (uint8_t)(0x40 | cmd);
    out[1] = (uint8_t)(arg >> 24);
    out[2] = (uint8_t)(arg >> 16);
    out[3] = (uint8_t)(arg >> 8);
    out[4] = (uint8_t)arg;
    out[5] = sd_crc7(out, 5);
}

/* Дисплей: 16-битные посылки — пиксель RGB565 одной записью в DR. Менять DFF только при SPE = 0. */
void spi1_pixels(uint16_t color, uint32_t n)
{
    SPI1->CR1 &= ~SPI_CR1_SPE;
    SPI1->CR1 |= SPI_CR1_DFF;
    SPI1->CR1 |= SPI_CR1_SPE;
    while (n--) {
        while (!(SPI1->SR & SPI_SR_TXE)) { }
        SPI1->DR = color;
    }
    spi1_done();
    (void)SPI1->DR;              /* принятое при передаче не нужно: сбросить RXNE и OVR */
    (void)SPI1->SR;
}

/* ── проверка расчётов ────────────────────────────────────────────────── */
_Static_assert(SPI_CR1_DFF == (1u << 11), "DFF — бит 11 у обеих серий");
_Static_assert(SPI_CR1_BR == (0x7u << 3), "делитель — биты 3–5");
_Static_assert(SPI_CR1_SSM == (1u << 9) && SPI_CR1_SSI == (1u << 8), "программный выбор");
_Static_assert(PCLK2_HZ / 2u == (PCLK2_HZ == 84000000u ? 42000000u : 36000000u), "SPI1 — половина APB2");
_Static_assert(PCLK1_HZ / 2u == (PCLK1_HZ == 42000000u ? 21000000u : 18000000u), "SPI2 — половина APB1");
/* Кадр 240 × 320 × 16 бит при делителе 2: миллисекунды на передачу без пауз. */
_Static_assert(240u * 320u * 16u / (PCLK2_HZ / 2u / 1000u) == (PCLK2_HZ == 84000000u ? 29u : 34u), "кадр, мс");

int main(void)
{
    uint8_t id[3], frame[6];
    static const uint8_t leds[3] = { 0xC3, 0x5A, 0x81 };
    spi1_init(0u);               /* делитель 2 */
    flash_jedec_id(id);
    hc595_write3(leds);
    sd_frame(0, 0, frame);
    spi1_pixels(0xF800u, 240u * 320u);
    for (;;) { }
}

spi_f0.c — SPI на STM32F072: поле DS и 8-битный доступ к DR.

/* Раздел 18.13: SPI на STM32F072 — SPI1 на PA5/PA6/PA7 (AF0), 8-битные посылки
 * через поле DS, обмен байтом через 8-битный доступ к DR. */
#include <stddef.h>
#include "stm32f0xx.h"

_Static_assert(offsetof(SPI_TypeDef, DR) == 0x0Cu && offsetof(SPI_TypeDef, SR) == 0x08u, "регистры как у F1");
_Static_assert(SPI_CR2_DS_Pos == 8u && (7u << SPI_CR2_DS_Pos) == 0x0700u, "DS = 0111 — 8 бит");
/* PCLK 48 МГц, делитель 8 (BR = 010) — 6 МГц на SCK */
#define PCLK_HZ 48000000u
_Static_assert(PCLK_HZ / 8u == 6000000u, "6 МГц");

void spi1_init_f0(void)
{
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    GPIOA->AFR[0] &= ~((0xFu << 20) | (0xFu << 24) | (0xFu << 28));          /* AF0 */
    GPIOA->MODER = (GPIOA->MODER & ~((3u << 10) | (3u << 12) | (3u << 14)))
                 | (2u << 10) | (2u << 12) | (2u << 14);
    SPI1->CR2 = (7u << SPI_CR2_DS_Pos) | SPI_CR2_FRXTH;  /* 8 бит; RXNE — как только есть байт */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1;   /* /8 */
    SPI1->CR1 |= SPI_CR1_SPE;
}

uint8_t spi1_xfer_f0(uint8_t out)
{
    *(volatile uint8_t *)&SPI1->DR = out;       /* именно байт: 16-битная запись кладёт в очередь два */
    while (!(SPI1->SR & SPI_SR_RXNE)) {
    }
    return *(volatile uint8_t *)&SPI1->DR;
}

spi_g0.c — тот же SPI на STM32G071.

/* Раздел 18.13: тот же SPI на STM32G071 — SPI1 на PA5/PA6/PA7 (AF0), такт с APBENR2. */
#include <stddef.h>
#include "stm32g0xx.h"

_Static_assert(offsetof(SPI_TypeDef, DR) == 0x0Cu, "блок тот же, что у F0");
_Static_assert((7u << SPI_CR2_DS_Pos) == 0x0700u, "DS = 0111 — 8 бит");
/* PCLK 64 МГц, делитель 8 — 8 МГц на SCK */
#define PCLK_HZ 64000000u
_Static_assert(PCLK_HZ / 8u == 8000000u, "8 МГц");

void spi1_init_g0(void)
{
    RCC->IOPENR |= RCC_IOPENR_GPIOAEN;
    RCC->APBENR2 |= RCC_APBENR2_SPI1EN;                                  /* у G0 — APBENR2 */
    GPIOA->AFR[0] &= ~((0xFu << 20) | (0xFu << 24) | (0xFu << 28));
    GPIOA->MODER = (GPIOA->MODER & ~((3u << 10) | (3u << 12) | (3u << 14)))
                 | (2u << 10) | (2u << 12) | (2u << 14);
    SPI1->CR2 = (7u << SPI_CR2_DS_Pos) | SPI_CR2_FRXTH;
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1;
    SPI1->CR1 |= SPI_CR1_SPE;
}

uint8_t spi1_xfer_g0(uint8_t out)
{
    *(volatile uint8_t *)&SPI1->DR = out;
    while (!(SPI1->SR & SPI_SR_RXNE)) {
    }
    return *(volatile uint8_t *)&SPI1->DR;
}

Дальше: раздел 18а «Звук по I2S: кадр, форматы и точная частота отсчётов» — второй режим модулей SPI.

Открыть в интерактивном справочнике