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

18з. MP3-декодер VS1053 по SPI: SCI и SDI, DREQ, загрузка потока с SD по DMA, громкость, сброс и патчи

В 16г звук давал готовый модуль DFPlayer: карта, декодер и усилитель внутри, микроконтроллер только шлёт по UART «играй файл 3». В 18а звук шёл по I2S: отсчёты считает сам STM32, а кодек их только выводит. VS1053b стоит посередине. Это декодер MP3, WAV, MIDI и других форматов с ЦАП и выходом на наушники, но файл ему отдаёт микроконтроллер — байт за байтом по SPI, со своей карты, из сети, откуда угодно. Здесь — драйвер: две шины на одном SPI, вывод DREQ, скорость SPI до и после настройки такта, сброс, громкость, чтение формата и битрейта, поток с SD-карты через кольцо и DMA так, чтобы задержка карты не рвала звук, правильная остановка трека и загрузка заплаток.

Опоры: RM0008 (DocID13902 Rev 17): без ремапа SPI1 — PA4–PA7, с. 185; SPI1_TX — DMA1, канал 3, таблица 78, с. 281; биты MINC, с. 285, и DIR, с. 286; SSM и SSI, с. 743; BR[2:0], с. 743; CPOL и CPHA, с. 744; TXDMAEN и флаг BSY, с. 745; флаги TXE и RXNE, с. 746. DocID13587 Rev 17 (STM32F103x8/xB): PA4–PA7 — без допуска 5 В, с. 29. Устройство SPI и DMA — разделы 18 и 9, файловая система — 42, карта по SPI — 18.10. Документа производителя на VS1053b в проекте нет, драйвера на диске тоже. Команды SCI, номера регистров и биты, адреса в памяти декодера, пределы скорости SPI, буфер 2048 байт, смысл DREQ, порядок остановки и формат заплаток — по документации производителя, не проверено; они сверены только со своей моделью микросхемы, в которую заложены те же сведения. Все числа ниже — прогон драйвера _verify/mp3_18i/vs18i.c, собранного arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2.22, против модели VS1053b, SD-карты и DMA на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Задержки SD-карты, время после записи регистра и поведение при слишком быстром SPI — допущения модели: эти числа говорят о модели, а не о микросхеме.

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

Оригинал Перевод
SCI — serial control interface шина команд: регистры по 16 бит, выбор xCS
SDI — serial data interface шина данных: байты файла, выбор xDCS
DREQ — data request вывод «примет ещё 32 байта»
XTALI, CLKI кварц декодера и такт его ядра после умножителя
clock multiplier (SC_MULT) умножитель такта в регистре CLOCKF
software reset (SM_RESET) программный сброс битом в MODE
new SDI mode (SM_SDINEW) «новый» режим: у данных свой выбор xDCS
cancel playback (SM_CANCEL) остановка декодирования с выбросом буфера
endFillByte байт-заполнитель после конца файла
stream header (HDAT0, HDAT1) заголовок потока: формат и битрейт
sine test (SM_TESTS) синус-тест: тон без файла
patch, plugin (VLSI) заплатка: код в память декодера по SCI

Вверху — подключение модуля VS1053b к STM32F103 по SPI1 в режиме 0: PA5 — SCK, PA6 — MISO, PA7 — MOSI, PA4 — xCS для шины команд SCI, PB0 — xDCS для шины данных SDI, PB1 — вход DREQ, он равен 1, когда в буфере декодера свободно не меньше 32 байт, PB10 — XRESET; на модуле кварц 12,288 МГц; команда SCI — 4 байта: 02 запись или 03 чтение, адрес и 16 бит старшим байтом вперёд; до записи CLOCKF запись идёт на 2,25 МГц, чтение на 1,125 МГц, после CLOCKF = 0x8800 и такта ядра 43,008 МГц — запись и данные на 9 МГц, чтение на 4,5 МГц; громкость VOL — шаг 0,5 дБ, 0xFE — тишина. Посередине — путь потока: SD-карта с FatFS и сектором 512 байт, кольцо 4096 байт в ОЗУ, которое пишет главный цикл, DMA1 канал 3 по 32 байта на xDCS, буфер декодера 2048 байт, декодер с ЦАП; фронт DREQ через EXTI1 запускает следующую посылку; 128 кбит/с — 16 000 байт/с, буфера декодера хватает на 128 мс, 320 кбит/с — 40 000 байт/с, 51,2 мс. Внизу — остановка трека: 2052 байта endFillByte из WRAM 0x1E06, затем SM_CANCEL и порции по 32 байта, пока бит не снимется, не снялся за 2048 байт — программный сброс; биты MODE: SM_SDINEW 0x0800, SM_RESET 0x0004, SM_CANCEL 0x0008, SM_TESTS 0x0020; заголовок MP3: HDAT1 = 0xFFFB — MPEG1 Layer III, HDAT0 = 0x9244 — 128 кбит/с, 44,1 кГц


18з.1 Устройство: две шины на одном SPI, DREQ и регистры

У VS1053b один SPI и два выбора (по документации производителя, не проверено):

Отсюда правило драйвера: данные — порциями по 32 байта и только при DREQ = 1; перед записью SCI — тоже дождаться DREQ.

регистр номер что в нём
MODE 0 режимы: SM_RESET 0x0004, SM_CANCEL 0x0008, SM_TESTS 0x0020, SM_SDINEW 0x0800; после сброса 0x4800
STATUS 1 биты 7:4 — версия, у VS1053 — 4
CLOCKF 3 SC_MULT (15:13) — умножитель кварца, SC_ADD (12:11), SC_FREQ (10:0) — нестандартный кварц
DECODE_TIME 4 секунд с начала декодирования
AUDATA 5 частота отсчётов (биты 15:1), бит 0 — стерео
WRAM, WRAMADDR 6, 7 окно в память декодера: адрес, затем слова, адрес растёт сам
HDAT0, HDAT1 8, 9 заголовок потока: у MP3 — первые 4 байта кадра
AIADDR 10 адрес запуска заплатки
VOL 11 ослабление: старший байт — левый канал, младший — правый

Модель — VS1053b как ведомый SPI: регистры, буфер 2048 байт, DREQ, разбор настоящих заголовков кадров MP3 с их битрейтом и длительностью, WAV и MIDI по заголовку, отмена, сброс, синус-тест, память декодера. Файлы MP3 для прогона она собирает сама: MPEG1 Layer III, 44,1 кГц, joint stereo, настоящие заголовки и длины кадров, вместо звука — байты 1…254. Звука модель не делает: «играть» для неё — забирать кадры из буфера в их темпе.


18з.2 Подключение к F103, сброс и запуск: скорость SPI до и после CLOCKF

SPI1 без ремапа — PA4–PA7 (с. 185), все четыре без допуска 5 В (с. 29), модуль питают от 3,3 В. PA5 — SCK, PA6 — MISO, PA7 — MOSI. Выборы xCS и xDCS — обычные выходы: PA4 и PB0, SSM = SSI = 1 (с. 743). DREQ — PB1 на вход с прерыванием EXTI1 по фронту, XRESET — PB10. Режим SPI — 0: CPOL = CPHA = 0 (с. 744).

Скорость. Такт ядра декодера CLKI после сброса равен кварцу XTALI, 12,288 МГц. Запись SCI и SDI — не быстрее CLKI/4, чтение SCI — не быстрее CLKI/7 (по документации производителя, не проверено). Регистр CLOCKF = 0x8800 — умножитель 3,5 и запас +1,0 для тяжёлых форматов (по документации, не проверено) — поднимает CLKI до 43,008 МГц. Делитель SPI1 — степень двойки от 72 МГц, и не быстрее 18 МГц (18.3). Драйвер выбирает самый быстрый, что не превышает предела:

uint32_t vs_spi_pick(uint32_t pclk_hz, uint32_t max_hz, uint32_t *br)
{
    uint32_t b;
    for (b = 0; b < 7u; b++) {                              /* делитель 2^(b+1): 2, 4 … 256 */
        uint32_t f = pclk_hz >> (b + 1u);
        if (f <= max_hz && f <= SPI_MAX_HZ)
            break;
    }
    *br = b;
    return pclk_hz >> (b + 1u);
}
CLKI запись: предел SPI1 чтение: предел SPI1
до CLOCKF 12,288 МГц 3,072 МГц 2,25 МГц, BR = 4 1,7554 МГц 1,125 МГц, BR = 5
после CLOCKF 43,008 МГц 10,752 МГц 9 МГц, BR = 2 6,144 МГц 4,5 МГц, BR = 3

Команда SCI — 32 бита: 14,22 мкс на 2,25 МГц и 3,56 мкс на 9 МГц.

Запуск. Аппаратный сброс XRESET, ожидание DREQ, проверка версии, MODE с SM_SDINEW, CLOCKF и проверка обоих чтением:

int vs_init(vs_t *vs, uint32_t pclk_hz, uint32_t flags)
{
    uint16_t st;
    int rc;
    vs->pclk_hz = pclk_hz;
    vs->spi_hz = 0;
    vs->dreq_waits = 0;
    if ((rc = vs_hw_reset(vs)) != VS_OK)
        return rc;                                          /* DREQ не поднялся: питание, XRESET, провод */
    st = vs_sci_read(vs, VS_STATUS);
    vs->version = (uint8_t)((st >> 4) & 15u);
    if (vs->version != 4u)
        return VS_E_ID;                                     /* 0xFFFF — MISO висит вверху: нет ответа */
    if (flags & VS_FIX_GPIO_BOOT) {                         /* модуль стартует в MIDI: GPIO — выходы с нулём */
        if ((rc = vs_wram_write(vs, VS_GPIO_DDR, 3u)) != VS_OK || (rc = vs_wram_write(vs, VS_GPIO_ODATA, 0u)) != VS_OK
            || (rc = vs_soft_reset(vs)) != VS_OK)
            return rc;
    } else {
        if ((rc = vs_sci_write(vs, VS_MODE, VS_MODE_RUN)) != VS_OK || (rc = vs_sci_write(vs, VS_CLOCKF, VS_CLOCKF_RUN)) != VS_OK)
            return rc;
    }
    if ((rc = vs_wait_dreq(vs, 100000u)) != VS_OK)          /* PLL декодера устанавливается */
        return rc;
    if (vs_sci_read(vs, VS_MODE) != VS_MODE_RUN || vs_sci_read(vs, VS_CLOCKF) != VS_CLOCKF_RUN)
        return VS_E_VERIFY;                                 /* запись не дошла: SPI быстрее, чем можно */
    return VS_OK;
}

Обмены запуска в модели:

посылка SCI ответ SPI что
03 01 00 00 0040 1,125 МГц STATUS: версия 4
02 00 08 00 — 2,25 МГц MODE = SM_SDINEW
02 03 88 00 — 2,25 МГц CLOCKF = 0x8800
03 00 00 00 0800 4,5 МГц MODE прочитан обратно
03 03 00 00 8800 4,5 МГц CLOCKF прочитан обратно

Весь запуск — 2988,1 мкс, из них XRESET внизу — 1 мс, остальное — DREQ после сброса: около 22 000 тактов XTALI, 1790,4 мкс (по документации производителя, не проверено; в модели — так). Драйвер ждал DREQ 173 раза по 10 мкс. Нет модуля — в модели MISO висит вверху, STATUS читается 0xFFFF, версия 15: VS_E_ID, −2. DREQ не поднимается — после 100 мс VS_E_TIMEOUT, −1 (в модели — через 111,0 мс вместе с опросами).

Сброс. Аппаратный — XRESET вниз и вверх; в модели он длился 2794,0 мкс и вернул CLOCKF к нулю. Программный — запись MODE с SM_RESET:

int vs_soft_reset(vs_t *vs)
{
    int rc;
    if ((rc = vs_sci_write(vs, VS_MODE, VS_MODE_RUN | SM_RESET)) != VS_OK)   /* SM_SDINEW — обязательно */
        return rc;
    vs_port_delay_us(2u);
    vs->clki_hz = VS_XTALI_HZ;                              /* снова медленно, пока не записан CLOCKF */
    if ((rc = vs_wait_dreq(vs, 100000u)) != VS_OK)
        return rc;
    return vs_sci_write(vs, VS_CLOCKF, VS_CLOCKF_RUN);
}

В модели — 1819,8 мкс, после него MODE = 0x0800 и CLOCKF = 0x8800. Бит SM_SDINEW пишут вместе с SM_RESET: 0x0804, а не 0x0004 (18з.7). После программного сброса модель возвращает CLOCKF к нулю (допущение модели), поэтому драйвер снова опускает SPI и пишет CLOCKF заново — так верно при любом поведении микросхемы.

Модуль и питание. На распространённых платах с VS1053b стоят кварц 12,288 МГц, стабилизаторы на 3,3 В и 1,8 В — ядро микросхемы питается от 1,8 В, аналоговая часть и выводы — от 3,3 В, — гнездо наушников и иногда слот microSD на том же SPI (по документации производителя и описаниям модулей, не проверено). Общий провод наушников на выходе — не земля, а вывод GBUF с половиной питания: соединять его с землёй усилителя нельзя (по документации, не проверено). Модель напряжений не считает.


18з.3 Громкость, формат, битрейт и синус-тест

Громкость. VOL — ослабление от максимума с шагом 0,5 дБ, старший байт — левый канал, младший — правый; 0xFE — тишина (по документации производителя, не проверено):

vs_set_volume VOL левый правый
0x00, 0x00 0x0000 0 дБ 0 дБ
0x14, 0x14 0x1414 −10 дБ −10 дБ
0x28, 0x28 0x2828 −20 дБ −20 дБ
0x00, 0xFE 0x00FE 0 дБ −127 дБ, тишина
0xFE, 0xFE 0xFEFE тишина тишина

Формат и битрейт. HDAT1 говорит, что за поток: у MP3 в нём слово синхронизации — одиннадцать единиц, версия и слой. HDAT0 — второе слово заголовка кадра: индекс битрейта в битах 15:12, индекс частоты — в 11:10. AUDATA — частота отсчётов и бит стерео, DECODE_TIME — секунды:

    if ((h1 & 0xFFE0u) == 0xFFE0u) {                        /* слово синхронизации MP3: 11 единиц */
        uint32_t id = (h1 >> 3) & 3u, idx = h0 >> 12;
        in->fmt = VS_FMT_MP3;
        in->mpeg = (uint8_t)(id == 3u ? 1 : id == 2u ? 2 : 25);
        in->layer = (uint8_t)(4u - ((h1 >> 1) & 3u));
        if (in->layer == 3u && idx < 15u)
            in->kbps = in->mpeg == 1u ? KBPS_V1[idx] : KBPS_V2[idx];
    } else if (h1 == 0x7665u) {
        in->fmt = VS_FMT_WAV;                               /* "ve" */
    } else if (h1 == 0x4D54u) {
        in->fmt = VS_FMT_MIDI;                              /* "MT" */
    } else if (h1 == 0x4F67u) {
        in->fmt = VS_FMT_OGG;                               /* "Og" */
    } else {
        in->fmt = h1 ? VS_FMT_OTHER : VS_FMT_NONE;
    }
    return VS_OK;

Модель посреди трека:

поток HDAT1 HDAT0 AUDATA драйвер прочёл
MP3 128 кбит/с 0xFFFB 0x9244 0xAC45 MPEG1, Layer III, 128 кбит/с, 44 100 Гц, стерео
MP3 320 кбит/с 0xFFFB 0xE244 0xAC45 MPEG1, Layer III, 320 кбит/с, 44 100 Гц, стерео
WAV 8 кГц, 16 бит, моно 0x7665 — 0x1F40 WAV, 8000 Гц, моно
MIDI 0x4D54 — — MIDI

Индекс 9 — 128 кбит/с, 14 — 320 кбит/с (таблица MPEG1 Layer III, по документации, не проверено). На середине файла в 3 с DECODE_TIME — 1. Переключать «режим MP3 или WAV» не нужно: формат декодер узнаёт сам по первым байтам, и после SM_CANCEL (18з.6) можно сразу слать файл другого формата. MIDI-файл модель только узнаёт, но не играет.

Синус-тест. С битом SM_TESTS в MODE восемь байт 53 EF 6E n 0 0 0 0 по SDI включают тон, 45 78 69 74 0 0 0 0 — выключают. Частота — Fs · S / 128, где Fs — по трём старшим битам n (44 100, 48 000, 32 000, 22 050, 24 000, 16 000, 11 025, 12 000 Гц), S — пять младших (по документации производителя, не проверено):

uint32_t vs_sine_hz(uint8_t n)
{
    return (uint32_t)SINE_FS[n >> 5] * (n & 31u) / 128u;    /* Fs · S / 128 */
}

n = 0x44 — 32 000 · 4 / 128 = 1000 Гц, n = 0x7E — 22 050 · 30 / 128 = 5167,97 Гц (драйвер считает целыми — 5167). Модель включила тон на той же частоте и выключила его, MODE вернулся к 0x0800. Без SM_TESTS те же восемь байт — просто данные: тон не включился. Синус-тест проверяет ЦАП, усилитель и наушники без файла и без SD-карты — первое, что стоит запустить на новой плате.


18з.4 Поток с SD: кольцо, DMA и прерывание DREQ

Файл читает FatFS (раздел 42) секторами по 512 байт в кольцо в ОЗУ. Декодеру его отдаёт vs_feed: пока DREQ = 1 и в кольце есть 32 байта, запускать посылку по DMA. Порция всегда целая: размер кольца — степень двойки, сектор и 32 его делят.

void vs_feed(vs_ring_t *r)
{
    uint32_t n;
    if (r->inflight) {
        if (vs_port_sdi_busy())
            return;                                         /* позовут по концу DMA */
        r->tail += r->inflight;
        r->inflight = 0;
    }
    n = r->head - r->tail;
    if (n < VS_CHUNK && !(r->eof && n)) {                   /* хвост файла короче 32 — отдать как есть */
        if (!r->eof && vs_port_dreq())
            r->starved++;                                   /* декодер ждёт, а в кольце пусто */
        return;
    }
    if (!vs_port_dreq())
        return;                                             /* буфер декодера полон: позовёт фронт DREQ */
    if (n > VS_CHUNK)
        n = VS_CHUNK;
    if (feed_vs)
        speed(feed_vs, VS_WRITE_DIV);
    r->inflight = n;
    r->chunks++;
    vs_port_sdi_start(r->buf + (r->tail & (r->size - 1u)), n);
}

Главный цикл — vs_pump: есть место под сектор — читать SD, иначе ждать:

int vs_pump(vs_ring_t *r, vs_read_fn rd, void *ctx)
{
    if (!r->eof && vs_ring_free(r) >= VS_SECTOR) {          /* место под сектор — читать SD */
        int n = rd(ctx, r->buf + (r->head & (r->size - 1u)), VS_SECTOR);
        if (n < 0)
            return VS_E_READ;
        r->head += (uint32_t)n;
        if (n < (int)VS_SECTOR)
            r->eof = 1;
        vs_port_kick();
        return 1;
    }
    if (r->eof && r->head == r->tail && !r->inflight)
        return 0;                                           /* всё ушло в декодер */
    vs_port_kick();
    vs_port_idle();
    return 1;
}

Чтение сектора SD длится то доли миллисекунды, то десятки (18з.5). Если vs_feed зовёт только главный цикл, на это время декодер остаётся без данных. Поэтому в прошивке её зовут прерывания: фронт DREQ (EXTI1) и конец посылки DMA (канал 3, SPI1_TX — с. 281):

void vs_port_sdi_start(const uint8_t *p, uint32_t n)
{
    GPIOB->BRR = 1u << XDCS_PIN;
    sdi_active = 1;
    DMA1_Channel3->CCR = 0;
    DMA1_Channel3->CMAR = (uint32_t)p;
    DMA1_Channel3->CNDTR = n;
    DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR | DMA_CCR3_TCIE | DMA_CCR3_EN;   /* память → SPI1->DR */
    SPI1->CR2 |= SPI_CR2_TXDMAEN;
}
void EXTI1_IRQHandler(void)
{
    EXTI->PR = 1u << DREQ_PIN;
    vs_feed(&ring);
}

DIR = 1 — из памяти в периферию, MINC — адрес памяти растёт (с. 286 и 285), TXDMAEN — запрос по TXE (с. 745). Принятое по MISO не нужно: по концу посылки драйвер читает DR и SR, как в 18.6. Команда SCI из главного цикла и посылка DMA делят один SPI: на время SCI порт выключает оба прерывания и ждёт конца посылки (vs_port_bus).

Прогон: файл 3 с, кольцо 4096 байт, кормление по прерываниям, SD без долгих задержек:

битрейт байт кадров посылок по 32 провалов сыграно SPI занят
128 кбит/с 48 065 115 1503 0 3004,082 мс 1,54 %
320 кбит/с 120 163 115 3756 0 3004,082 мс 3,74 %

Кадр MP3 на 44,1 кГц — 1152 отсчёта, 26,122 мс; на 128 кбит/с в нём 417 или 418 байт, на 320 — 1044 или 1045. Порция 32 байта на 9 МГц — 28,44 мкс, на 2,25 МГц — 113,78 мкс: шина не узкое место, узкое — SD.


18з.5 Сколько буфера нужно: битрейт и задержки SD

Битрейт — бит в секунду, в байтах — в 8 раз меньше: 128 кбит/с — 16 000 байт/с, 320 кбит/с — 40 000 байт/с. Буфера декодера, 2048 байт, хватает на 128 мс при 128 кбит/с и на 51,2 мс при 320.

Карта отвечает неровно: обычно сектор за доли миллисекунды, но иногда — десятки миллисекунд (в модели: 0,8 мс и каждый 40-й сектор ещё 100 мс, допущение модели). Пока карта молчит, звук берётся из буферов. На 100 мс при 320 кбит/с нужно 4000 байт. Буфер декодера даёт 2048 — мало. Кольцо при чтении секторами заполнено не меньше чем на свой размер минус 511 байт: 2048 + 2048 − 511 = 3585 — тоже мало; 2048 + 4096 − 511 = 5633 — хватает. При 128 кбит/с 100 мс — 1600 байт, и хватает одного буфера декодера. Модель, файл 4 с:

битрейт кормление кольцо 1024 2048 4096 8192
128 кбит/с, 3 задержки из цикла 0 0 0 0
128 кбит/с, 3 задержки прерывания 0 0 0 0
320 кбит/с, 7 задержек из цикла 7, 353,151 мс 7, 353,183 мс 7, 353,151 мс 7, 354,984 мс
320 кбит/с, 7 задержек прерывания 7, 270,582 мс 4, 92,187 мс 0 0

В клетке — сколько раз декодер остался без кадра и сколько длилась тишина. Кормление из главного цикла на 320 кбит/с не спасает никакое кольцо: пока цикл стоит в f_read, кольцо никто не отдаёт. С прерываниями хватает 4096 байт — у BluePill это пятая часть ОЗУ. Чем больше задержки карты, тем больше кольцо; их длину и частоту стоит замерить на своей карте.


18з.6 Остановка трека: endFillByte, SM_CANCEL и заплатки

Конец файла. Последний кадр декодер доиграет, только если за ним придут ещё байты. Поэтому после файла шлют 2052 байта endFillByte — его значение лежит в памяти декодера по адресу 0x1E06, — затем ставят SM_CANCEL и шлют по 32 байта, пока бит не снимется; не снялся за 2048 байт — программный сброс (по документации производителя, не проверено):

int vs_finish(vs_t *vs)
{
    uint8_t f[VS_CHUNK];
    uint8_t efb = (uint8_t)vs_wram_read(vs, VS_PAR_END_FILL);
    int rc;
    for (uint32_t i = 0; i < VS_CHUNK; i++)
        f[i] = efb;
    for (uint32_t i = 0; i < VS_FILL_END; i += VS_CHUNK)     /* 2052 байта endFillByte: декодер доиграет */
        if ((rc = sdi_send(vs, f, VS_FILL_END - i < VS_CHUNK ? VS_FILL_END - i : VS_CHUNK)) != VS_OK)
            return rc;
    if ((rc = vs_sci_write(vs, VS_MODE, (uint16_t)(vs->mode | SM_CANCEL))) != VS_OK)
        return rc;
    for (uint32_t sent = 0; sent < VS_CANCEL_MAX; sent += VS_CHUNK) {
        if ((rc = sdi_send(vs, f, VS_CHUNK)) != VS_OK)
            return rc;
        if (!(vs_sci_read(vs, VS_MODE) & SM_CANCEL))
            return (int)(sent / VS_CHUNK + 1u);             /* сколько порций после SM_CANCEL */
    }
    (void)vs_soft_reset(vs);                                /* не снялся за 2048 байт */
    return VS_E_CANCEL;
}

В модели endFillByte — 0x00. После 3-секундного трека SM_CANCEL снялся после первой же порции, все 115 кадров сыграны, в буфере — 0 байт, HDAT1 обнулён.

Посреди трека. vs_cancel останавливает кормление, ставит SM_CANCEL и шлёт остаток файла из кольца, проверяя бит после каждой порции; когда он снялся, HDAT0 и HDAT1 должны быть нулями, иначе — сброс. В модели остановка через 1 с: бит снялся после 14 порций — декодер выбросил буфер на конце звучащего кадра, — сыграно 35 кадров, HDAT0 = HDAT1 = 0.

Без этого. Тот же трек просто бросили — перестали слать данные и сразу начали следующий. Модель: через 300 мс в буфере так и лежали 374 байта — неполный последний кадр ждал продолжения. Следующий файл дописался к нему: вышел один кадр из двух файлов, затем 373 байта мусора до ближайшего заголовка — первый кадр второго трека пропал. Испорченный кадр — это щелчок. С vs_cancel перед следующим файлом — 0 испорченных кадров.

Заплатки. VLSI выпускает для VS1053b заплатки и плагины — исправления и новые возможности, например декодеры других форматов. Это код в память декодера; его грузят по SCI после каждого сброса, потому что память стирается. Формат — массив 16-битных слов: номер регистра, число, значения; старший бит числа — повторить одно значение (по документации производителя, не проверено):

int vs_load_patch(vs_t *vs, const uint16_t *p, uint32_t n)
{
    uint32_t i = 0;
    int rc;
    while (i < n) {
        uint16_t addr, cnt;
        if (i + 2u > n)
            return VS_E_ARG;
        addr = p[i++];
        cnt = p[i++];
        if (cnt & 0x8000u) {                                /* сжатие: одно значение cnt раз */
            cnt &= 0x7FFFu;
            if (i >= n)
                return VS_E_ARG;
            while (cnt--)
                if ((rc = vs_sci_write(vs, (uint8_t)addr, p[i])) != VS_OK)
                    return rc;
            i++;
        } else {                                            /* cnt значений подряд в один регистр */
            if (i + cnt > n)
                return VS_E_ARG;
            while (cnt--)
                if ((rc = vs_sci_write(vs, (uint8_t)addr, p[i++])) != VS_OK)
                    return rc;
        }
    }
    return VS_OK;
}

Настоящих заплаток VLSI на диске нет, загрузчик проверен своей: адрес 0x8010 в WRAMADDR, четыре слова подряд, ещё три — одним повтором, и запуск AIADDR = 0x0050. Модель получила 9 записей SCI, в памяти — 1111, 2222, 3333, 4444, 00AB, 00AB, 00AB, адрес запуска 0x0050. Обрезанный массив — VS_E_ARG, −5.


18з.7 Грабли

Быстрый SPI до CLOCKF. Сразу 9 МГц после сброса: в модели обе записи — MODE и CLOCKF — потерялись (допущение модели: слишком быстрая запись теряется), CLOCKF остался 0x0000, а MODE, прочитанный на 9 МГц, — 0x2400 вместо 0x4800 (допущение: слишком быстрое чтение сдвигает слово). Тот же MODE на 1,125 МГц — 0x4800. vs_init читает MODE и CLOCKF обратно: на такой ошибке он вернёт VS_E_VERIFY, −3.

Перепутаны xCS и xDCS. Запись громкости 02 0B 20 20 ушла на xDCS: VOL остался 0x0000, а четыре байта легли в буфер потока как данные. Начало кадра MP3 FF FB 90 44 ушло на xCS: декодер не узнал команду и промолчал, HDAT1 = 0x0000. Ни ошибки, ни звука.

Не ждать DREQ. Наивный цикл: сектор с SD — 16 порций подряд без проверки DREQ. Файл 2 с, 32 182 байта; в модели из них 28 473 байта пропали на полном буфере, декодер нашёл 8 кадров из 77 и 3 раза сбился. DREQ — не пожелание, а условие.

«Старый» режим SDI. Программный сброс записью 0x0004 снял SM_SDINEW: MODE = 0x0000. Без него xDCS не работает (по документации производителя; в модели байты без SM_SDINEW пропадают). Секундный файл ушёл в никуда: 0 кадров, DECODE_TIME 0, SM_CANCEL не снялся за 2048 байт, и vs_finish сделал программный сброс, VS_E_CANCEL, −4. В MODE всегда пишут SM_SDINEW: 0x0800, при сбросе — 0x0804.

Модуль стартует в MIDI. У части модулей вывод GPIO1 подтянут, и декодер просыпается в режиме MIDI реального времени: байты MP3 он принимает за ноты (известная беда таких модулей, не проверено). В модели такой модуль сыграл 0 кадров из 38. Исправление — в vs_init с флагом VS_FIX_GPIO_BOOT: в WRAM 0xC017 = 3 и 0xC019 = 0, то есть GPIO0 и GPIO1 — выходы с нулём, затем программный сброс; после него — 38 кадров из 38.

Щелчок при остановке. Трек бросили без endFillByte и SM_CANCEL — хвост кадра смешался со следующим файлом (18з.6). Остановка — только через vs_finish или vs_cancel.

Кормление из главного цикла. На 320 кбит/с любая задержка SD длиннее 51,2 мс — тишина, и кольцо не помогает (18з.5). Посылки — из прерываний DREQ и DMA.

SCI посреди посылки DMA. Чтение HDAT или запись VOL во время посылки SDI портит обе: выборы разные, а SPI один. В драйвере SCI берёт шину через vs_port_bus: прерывания кормления выключены, посылка DMA дожата.


18з.8 Итоги

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

vs18i.h — регистры и биты VS1053b, структуры, интерфейс драйвера и платформы.

/* vs18i.h — раздел 18з: драйвер MP3-декодера VS1053b по SPI без регистров STM32.
 * Две шины на одном SPI: SCI (команды, выбор xCS, регистры по 16 бит) и SDI (поток файла, выбор xDCS).
 * Платформа даёт обмен SCI, запуск посылки SDI по DMA, вывод DREQ, вывод XRESET, паузу и скорость SPI:
 * в прошивке fw18i.c — SPI1, DMA1 и выводы STM32F103, в стенде host18i.c — модель model18i.py.
 * Номера регистров, биты, адреса в памяти и порядок действий — по документации производителя (описания
 * VS1053b в проекте нет, не проверено), сверены только с моделью. */
#ifndef VS18I_H
#define VS18I_H
#include <stdint.h>

/* ── регистры SCI ─────────────────────────────────────────────────────── */
#define VS_MODE         0x0u            /* режимы: SM_RESET, SM_CANCEL, SM_TESTS, SM_SDINEW */
#define VS_STATUS       0x1u            /* SS_VER (биты 7:4) — 4 у VS1053 */
#define VS_BASS         0x2u
#define VS_CLOCKF       0x3u            /* SC_MULT (15:13), SC_ADD (12:11), SC_FREQ (10:0) */
#define VS_DECODE_TIME  0x4u            /* секунд с начала декодирования */
#define VS_AUDATA       0x5u            /* частота отсчётов (15:1), бит 0 — стерео */
#define VS_WRAM         0x6u            /* окно в память декодера */
#define VS_WRAMADDR     0x7u            /* адрес для WRAM, растёт сам */
#define VS_HDAT0        0x8u            /* заголовок потока: у MP3 — битрейт, частота, режим */
#define VS_HDAT1        0x9u            /* у MP3 — слово синхронизации, версия, слой */
#define VS_AIADDR       0xAu            /* адрес запуска заплатки */
#define VS_VOL          0xBu            /* ослабление: старший байт — левый, младший — правый, шаг 0,5 дБ */

#define VS_SCI_WRITE    0x02u
#define VS_SCI_READ     0x03u

/* ── биты MODE ────────────────────────────────────────────────────────── */
#define SM_RESET        0x0004u         /* программный сброс, сам снимается */
#define SM_CANCEL       0x0008u         /* остановить декодирование, сам снимается */
#define SM_TESTS        0x0020u         /* разрешить тесты (синус) */
#define SM_SDINEW       0x0800u         /* «новый» режим SDI: свой выбор xDCS */
#define VS_MODE_RUN     SM_SDINEW       /* что пишет драйвер в MODE в работе */

/* ── тактирование и скорость SPI ──────────────────────────────────────── */
#define VS_XTALI_HZ     12288000u       /* кварц модуля */
#define VS_CLOCKF_RUN   0x8800u         /* SC_MULT = 3,5×, SC_ADD = +1,0× */
#define VS_WRITE_DIV    4u              /* запись SCI и SDI: не быстрее CLKI/4 */
#define VS_READ_DIV     7u              /* чтение SCI: не быстрее CLKI/7 */

/* ── память декодера ──────────────────────────────────────────────────── */
#define VS_PAR_END_FILL 0x1E06u         /* endFillByte: младший байт */
#define VS_GPIO_DDR     0xC017u         /* направление GPIO */
#define VS_GPIO_ODATA   0xC019u         /* выходы GPIO */

/* ── поток ────────────────────────────────────────────────────────────── */
#define VS_CHUNK        32u             /* DREQ = 1 — примет не меньше 32 байт */
#define VS_SECTOR       512u            /* порция чтения с SD */
#define VS_FILL_END     2052u           /* endFillByte после конца файла */
#define VS_CANCEL_MAX   2048u           /* SM_CANCEL не снялся за 2048 байт — программный сброс */

enum { VS_OK = 0, VS_E_TIMEOUT = -1, VS_E_ID = -2, VS_E_VERIFY = -3, VS_E_CANCEL = -4, VS_E_ARG = -5, VS_E_READ = -6 };
enum { VS_FMT_NONE = 0, VS_FMT_MP3, VS_FMT_WAV, VS_FMT_MIDI, VS_FMT_OGG, VS_FMT_OTHER };
#define VS_FIX_GPIO_BOOT 1u             /* флаг vs_init: модуль стартует в MIDI — прижать GPIO и сбросить */

typedef struct {
    uint32_t clki_hz;                   /* такт ядра декодера: XTALI до CLOCKF, 3,5 × XTALI после */
    uint32_t spi_hz;                    /* что сейчас стоит на SPI */
    uint32_t pclk_hz;                   /* такт шины SPI в STM32, для выбора делителя */
    uint16_t mode;                      /* копия MODE */
    uint8_t version;                    /* SS_VER из STATUS */
    uint32_t dreq_waits;                /* сколько раз ждали DREQ */
} vs_t;

typedef struct {
    uint8_t *buf;
    uint32_t size;                      /* степень двойки, кратна 512 */
    volatile uint32_t head;             /* счётчик записанных байт: пишет главный цикл (SD) */
    volatile uint32_t tail;             /* счётчик отданных декодеру: пишет vs_feed */
    volatile uint32_t inflight;         /* байт в посылке DMA, 0 — DMA свободен */
    volatile uint32_t starved;          /* DREQ = 1, а в кольце меньше 32 байт */
    uint32_t eof;                       /* файл дочитан */
    uint32_t chunks;                    /* сколько посылок по 32 байта ушло */
} vs_ring_t;

typedef struct {
    uint8_t fmt;                        /* VS_FMT_… по HDAT1 */
    uint8_t mpeg;                       /* 1 — MPEG1, 2 — MPEG2, 25 — MPEG2.5 */
    uint8_t layer;
    uint8_t channels;                   /* 1 или 2 — по AUDATA */
    uint16_t kbps;                      /* битрейт MP3 из HDAT0 */
    uint32_t fs;                        /* частота отсчётов из AUDATA */
    uint16_t hdat0, hdat1, audata, dtime;
} vs_info_t;

typedef int (*vs_read_fn)(void *ctx, uint8_t *dst, uint32_t n);     /* сколько прочитано, < 0 — ошибка */

/* ── платформа: прошивка или стенд ────────────────────────────────────── */
void vs_port_sci(const uint8_t tx[4], uint8_t rx[4]);   /* xCS вниз, 4 байта в обе стороны, xCS вверх */
void vs_port_sdi_start(const uint8_t *p, uint32_t n);  /* xDCS вниз, n байт по DMA; xDCS вверх — по концу */
int vs_port_sdi_busy(void);                             /* посылка SDI ещё идёт */
int vs_port_dreq(void);                                 /* уровень DREQ */
void vs_port_xreset(int level);
void vs_port_delay_us(uint32_t us);
void vs_port_spi_hz(uint32_t hz, uint32_t br);         /* поставить SPI на hz (делитель 2^(br+1)) */
void vs_port_bus(int take);                             /* 1 — SPI нужен для SCI: остановить кормление */
void vs_port_kick(void);                                /* вызвать vs_feed: прерывание или прямо */
void vs_port_idle(void);                                /* делать нечего: ждать события */

/* ── скорость SPI ─────────────────────────────────────────────────────── */
uint32_t vs_spi_pick(uint32_t pclk_hz, uint32_t max_hz, uint32_t *br);   /* самый быстрый делитель ≤ max_hz */
uint32_t vs_clki(uint16_t clockf);                                      /* CLKI по CLOCKF, Гц */

/* ── SCI ──────────────────────────────────────────────────────────────── */
int vs_wait_dreq(vs_t *vs, uint32_t timeout_us);
int vs_sci_write(vs_t *vs, uint8_t reg, uint16_t v);
uint16_t vs_sci_read(vs_t *vs, uint8_t reg);
uint16_t vs_wram_read(vs_t *vs, uint16_t addr);
int vs_wram_write(vs_t *vs, uint16_t addr, uint16_t v);

/* ── управление ───────────────────────────────────────────────────────── */
int vs_hw_reset(vs_t *vs);
int vs_soft_reset(vs_t *vs);
int vs_init(vs_t *vs, uint32_t pclk_hz, uint32_t flags);
int vs_set_volume(vs_t *vs, uint8_t left, uint8_t right);   /* шаги по 0,5 дБ; 0xFE — тишина */
int vs_read_info(vs_t *vs, vs_info_t *in);
uint32_t vs_sine_hz(uint8_t n);                              /* частота синус-теста для байта n */
int vs_sine_test(vs_t *vs, uint8_t n, int on);
int vs_load_patch(vs_t *vs, const uint16_t *p, uint32_t n); /* сжатый формат заплаток: addr, n, данные */

/* ── поток ────────────────────────────────────────────────────────────── */
void vs_stream_bind(vs_t *vs);                                 /* чей SPI настраивать в vs_feed */
void vs_ring_init(vs_ring_t *r, uint8_t *buf, uint32_t size);
uint32_t vs_ring_free(const vs_ring_t *r);
void vs_feed(vs_ring_t *r);                                 /* из прерываний DREQ и DMA или из цикла */
int vs_pump(vs_ring_t *r, vs_read_fn rd, void *ctx);        /* шаг главного цикла: 1 — играет, 0 — всё отдано */
int vs_finish(vs_t *vs);                                    /* конец файла: endFillByte и SM_CANCEL */
int vs_cancel(vs_t *vs, vs_ring_t *r);                      /* остановка посреди трека */

#endif

vs18i.c — SCI, скорость SPI, сброс и запуск, громкость, формат, синус-тест, заплатки, кольцо, DMA и остановка.

/* vs18i.c — раздел 18з: драйвер VS1053b. Только SCI, SDI и DREQ; шину и выводы даёт платформа. */
#include "vs18i.h"

#define SPI_MAX_HZ 18000000u            /* F103: SPI не быстрее 18 МГц (раздел 18.3) */

static const uint8_t SC_MULT_X10[8] = {10, 20, 25, 30, 35, 40, 45, 50};
static const uint16_t KBPS_V1[15] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
static const uint16_t KBPS_V2[15] = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
static const uint16_t SINE_FS[8] = {44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000};

/* ── скорость SPI ─────────────────────────────────────────────────────── */
uint32_t vs_spi_pick(uint32_t pclk_hz, uint32_t max_hz, uint32_t *br)
{
    uint32_t b;
    for (b = 0; b < 7u; b++) {                              /* делитель 2^(b+1): 2, 4 … 256 */
        uint32_t f = pclk_hz >> (b + 1u);
        if (f <= max_hz && f <= SPI_MAX_HZ)
            break;
    }
    *br = b;
    return pclk_hz >> (b + 1u);
}

uint32_t vs_clki(uint16_t clockf)
{
    uint32_t sc_freq = clockf & 0x7FFu;
    uint32_t xtali = sc_freq ? 8000000u + sc_freq * 4000u : VS_XTALI_HZ;    /* SC_FREQ = 0 — кварц 12,288 МГц */
    return xtali / 10u * SC_MULT_X10[clockf >> 13];
}

static void speed(vs_t *vs, uint32_t div)
{
    uint32_t br, hz = vs_spi_pick(vs->pclk_hz, vs->clki_hz / div, &br);
    if (hz != vs->spi_hz) {
        vs_port_spi_hz(hz, br);
        vs->spi_hz = hz;
    }
}

/* ── SCI ──────────────────────────────────────────────────────────────── */
int vs_wait_dreq(vs_t *vs, uint32_t timeout_us)
{
    for (uint32_t t = 0; !vs_port_dreq(); t += 10u) {
        if (t >= timeout_us)
            return VS_E_TIMEOUT;
        vs->dreq_waits++;
        vs_port_delay_us(10u);
    }
    return VS_OK;
}

int vs_sci_write(vs_t *vs, uint8_t reg, uint16_t v)
{
    uint8_t tx[4] = {VS_SCI_WRITE, reg, (uint8_t)(v >> 8), (uint8_t)v}, rx[4];
    int rc;
    vs_port_bus(1);                                         /* SPI занят: кормление по DMA ждёт */
    if ((rc = vs_wait_dreq(vs, 100000u)) != VS_OK) {        /* DREQ = 0 — прошлая команда ещё идёт */
        vs_port_bus(0);
        return rc;
    }
    speed(vs, VS_WRITE_DIV);
    vs_port_sci(tx, rx);
    vs_port_bus(0);
    if (reg == VS_MODE)
        vs->mode = (uint16_t)(v & ~(SM_RESET | SM_CANCEL));
    if (reg == VS_CLOCKF)
        vs->clki_hz = vs_clki(v);                           /* дальше SPI можно быстрее */
    return VS_OK;
}

uint16_t vs_sci_read(vs_t *vs, uint8_t reg)
{
    uint8_t tx[4] = {VS_SCI_READ, reg, 0, 0}, rx[4];
    vs_port_bus(1);
    speed(vs, VS_READ_DIV);                                 /* чтение медленнее записи: CLKI/7 */
    vs_port_sci(tx, rx);
    vs_port_bus(0);
    return (uint16_t)(rx[2] << 8 | rx[3]);
}

uint16_t vs_wram_read(vs_t *vs, uint16_t addr)
{
    if (vs_sci_write(vs, VS_WRAMADDR, addr) != VS_OK)
        return 0xFFFFu;
    return vs_sci_read(vs, VS_WRAM);
}

int vs_wram_write(vs_t *vs, uint16_t addr, uint16_t v)
{
    int rc = vs_sci_write(vs, VS_WRAMADDR, addr);
    return rc != VS_OK ? rc : vs_sci_write(vs, VS_WRAM, v);
}

/* ── сброс и запуск ───────────────────────────────────────────────────── */
int vs_hw_reset(vs_t *vs)
{
    vs_port_xreset(0);
    vs_port_delay_us(1000u);
    vs_port_xreset(1);
    vs->clki_hz = VS_XTALI_HZ;                              /* после сброса CLKI = XTALI */
    vs->mode = VS_MODE_RUN;
    return vs_wait_dreq(vs, 100000u);                       /* DREQ поднимется, когда декодер готов */
}

int vs_soft_reset(vs_t *vs)
{
    int rc;
    if ((rc = vs_sci_write(vs, VS_MODE, VS_MODE_RUN | SM_RESET)) != VS_OK)   /* SM_SDINEW — обязательно */
        return rc;
    vs_port_delay_us(2u);
    vs->clki_hz = VS_XTALI_HZ;                              /* снова медленно, пока не записан CLOCKF */
    if ((rc = vs_wait_dreq(vs, 100000u)) != VS_OK)
        return rc;
    return vs_sci_write(vs, VS_CLOCKF, VS_CLOCKF_RUN);
}

int vs_init(vs_t *vs, uint32_t pclk_hz, uint32_t flags)
{
    uint16_t st;
    int rc;
    vs->pclk_hz = pclk_hz;
    vs->spi_hz = 0;
    vs->dreq_waits = 0;
    if ((rc = vs_hw_reset(vs)) != VS_OK)
        return rc;                                          /* DREQ не поднялся: питание, XRESET, провод */
    st = vs_sci_read(vs, VS_STATUS);
    vs->version = (uint8_t)((st >> 4) & 15u);
    if (vs->version != 4u)
        return VS_E_ID;                                     /* 0xFFFF — MISO висит вверху: нет ответа */
    if (flags & VS_FIX_GPIO_BOOT) {                         /* модуль стартует в MIDI: GPIO — выходы с нулём */
        if ((rc = vs_wram_write(vs, VS_GPIO_DDR, 3u)) != VS_OK || (rc = vs_wram_write(vs, VS_GPIO_ODATA, 0u)) != VS_OK
            || (rc = vs_soft_reset(vs)) != VS_OK)
            return rc;
    } else {
        if ((rc = vs_sci_write(vs, VS_MODE, VS_MODE_RUN)) != VS_OK || (rc = vs_sci_write(vs, VS_CLOCKF, VS_CLOCKF_RUN)) != VS_OK)
            return rc;
    }
    if ((rc = vs_wait_dreq(vs, 100000u)) != VS_OK)          /* PLL декодера устанавливается */
        return rc;
    if (vs_sci_read(vs, VS_MODE) != VS_MODE_RUN || vs_sci_read(vs, VS_CLOCKF) != VS_CLOCKF_RUN)
        return VS_E_VERIFY;                                 /* запись не дошла: SPI быстрее, чем можно */
    return VS_OK;
}

int vs_set_volume(vs_t *vs, uint8_t left, uint8_t right)
{
    return vs_sci_write(vs, VS_VOL, (uint16_t)(left << 8 | right));
}

int vs_read_info(vs_t *vs, vs_info_t *in)
{
    uint16_t h1 = vs_sci_read(vs, VS_HDAT1), h0 = vs_sci_read(vs, VS_HDAT0);
    in->hdat0 = h0;
    in->hdat1 = h1;
    in->audata = vs_sci_read(vs, VS_AUDATA);
    in->dtime = vs_sci_read(vs, VS_DECODE_TIME);
    in->fs = in->audata & 0xFFFEu;
    in->channels = (uint8_t)((in->audata & 1u) ? 2 : 1);
    in->mpeg = in->layer = 0;
    in->kbps = 0;
    if ((h1 & 0xFFE0u) == 0xFFE0u) {                        /* слово синхронизации MP3: 11 единиц */
        uint32_t id = (h1 >> 3) & 3u, idx = h0 >> 12;
        in->fmt = VS_FMT_MP3;
        in->mpeg = (uint8_t)(id == 3u ? 1 : id == 2u ? 2 : 25);
        in->layer = (uint8_t)(4u - ((h1 >> 1) & 3u));
        if (in->layer == 3u && idx < 15u)
            in->kbps = in->mpeg == 1u ? KBPS_V1[idx] : KBPS_V2[idx];
    } else if (h1 == 0x7665u) {
        in->fmt = VS_FMT_WAV;                               /* "ve" */
    } else if (h1 == 0x4D54u) {
        in->fmt = VS_FMT_MIDI;                              /* "MT" */
    } else if (h1 == 0x4F67u) {
        in->fmt = VS_FMT_OGG;                               /* "Og" */
    } else {
        in->fmt = h1 ? VS_FMT_OTHER : VS_FMT_NONE;
    }
    return VS_OK;
}

/* ── посылка SDI с ожиданием: тесты и конец трека ─────────────────────── */
static int sdi_send(vs_t *vs, const uint8_t *p, uint32_t n)
{
    int rc;
    vs_port_bus(1);
    if ((rc = vs_wait_dreq(vs, 100000u)) == VS_OK) {
        speed(vs, VS_WRITE_DIV);
        vs_port_sdi_start(p, n);
        while (vs_port_sdi_busy()) {
        }
    }
    vs_port_bus(0);
    return rc;
}

uint32_t vs_sine_hz(uint8_t n)
{
    return (uint32_t)SINE_FS[n >> 5] * (n & 31u) / 128u;    /* Fs · S / 128 */
}

int vs_sine_test(vs_t *vs, uint8_t n, int on)
{
    static const uint8_t off_seq[8] = {0x45, 0x78, 0x69, 0x74, 0, 0, 0, 0};   /* "Exit" */
    uint8_t on_seq[8] = {0x53, 0xEF, 0x6E, n, 0, 0, 0, 0};
    int rc;
    if (on) {
        if ((rc = vs_sci_write(vs, VS_MODE, VS_MODE_RUN | SM_TESTS)) != VS_OK)   /* без SM_TESTS — просто данные */
            return rc;
        return sdi_send(vs, on_seq, 8);
    }
    if ((rc = sdi_send(vs, off_seq, 8)) != VS_OK)
        return rc;
    return vs_sci_write(vs, VS_MODE, VS_MODE_RUN);
}

int vs_load_patch(vs_t *vs, const uint16_t *p, uint32_t n)
{
    uint32_t i = 0;
    int rc;
    while (i < n) {
        uint16_t addr, cnt;
        if (i + 2u > n)
            return VS_E_ARG;
        addr = p[i++];
        cnt = p[i++];
        if (cnt & 0x8000u) {                                /* сжатие: одно значение cnt раз */
            cnt &= 0x7FFFu;
            if (i >= n)
                return VS_E_ARG;
            while (cnt--)
                if ((rc = vs_sci_write(vs, (uint8_t)addr, p[i])) != VS_OK)
                    return rc;
            i++;
        } else {                                            /* cnt значений подряд в один регистр */
            if (i + cnt > n)
                return VS_E_ARG;
            while (cnt--)
                if ((rc = vs_sci_write(vs, (uint8_t)addr, p[i++])) != VS_OK)
                    return rc;
        }
    }
    return VS_OK;
}

/* ── поток: кольцо, DMA и DREQ ────────────────────────────────────────── */
static vs_t *feed_vs;                                       /* для скорости SPI в vs_feed */

void vs_ring_init(vs_ring_t *r, uint8_t *buf, uint32_t size)
{
    r->buf = buf;
    r->size = size;
    r->head = r->tail = r->inflight = r->starved = 0;
    r->eof = r->chunks = 0;
}

uint32_t vs_ring_free(const vs_ring_t *r)
{
    return r->size - (r->head - r->tail);                   /* байты в полёте DMA ещё заняты */
}

void vs_feed(vs_ring_t *r)
{
    uint32_t n;
    if (r->inflight) {
        if (vs_port_sdi_busy())
            return;                                         /* позовут по концу DMA */
        r->tail += r->inflight;
        r->inflight = 0;
    }
    n = r->head - r->tail;
    if (n < VS_CHUNK && !(r->eof && n)) {                   /* хвост файла короче 32 — отдать как есть */
        if (!r->eof && vs_port_dreq())
            r->starved++;                                   /* декодер ждёт, а в кольце пусто */
        return;
    }
    if (!vs_port_dreq())
        return;                                             /* буфер декодера полон: позовёт фронт DREQ */
    if (n > VS_CHUNK)
        n = VS_CHUNK;
    if (feed_vs)
        speed(feed_vs, VS_WRITE_DIV);
    r->inflight = n;
    r->chunks++;
    vs_port_sdi_start(r->buf + (r->tail & (r->size - 1u)), n);
}

int vs_pump(vs_ring_t *r, vs_read_fn rd, void *ctx)
{
    if (!r->eof && vs_ring_free(r) >= VS_SECTOR) {          /* место под сектор — читать SD */
        int n = rd(ctx, r->buf + (r->head & (r->size - 1u)), VS_SECTOR);
        if (n < 0)
            return VS_E_READ;
        r->head += (uint32_t)n;
        if (n < (int)VS_SECTOR)
            r->eof = 1;
        vs_port_kick();
        return 1;
    }
    if (r->eof && r->head == r->tail && !r->inflight)
        return 0;                                           /* всё ушло в декодер */
    vs_port_kick();
    vs_port_idle();
    return 1;
}

int vs_finish(vs_t *vs)
{
    uint8_t f[VS_CHUNK];
    uint8_t efb = (uint8_t)vs_wram_read(vs, VS_PAR_END_FILL);
    int rc;
    for (uint32_t i = 0; i < VS_CHUNK; i++)
        f[i] = efb;
    for (uint32_t i = 0; i < VS_FILL_END; i += VS_CHUNK)     /* 2052 байта endFillByte: декодер доиграет */
        if ((rc = sdi_send(vs, f, VS_FILL_END - i < VS_CHUNK ? VS_FILL_END - i : VS_CHUNK)) != VS_OK)
            return rc;
    if ((rc = vs_sci_write(vs, VS_MODE, (uint16_t)(vs->mode | SM_CANCEL))) != VS_OK)
        return rc;
    for (uint32_t sent = 0; sent < VS_CANCEL_MAX; sent += VS_CHUNK) {
        if ((rc = sdi_send(vs, f, VS_CHUNK)) != VS_OK)
            return rc;
        if (!(vs_sci_read(vs, VS_MODE) & SM_CANCEL))
            return (int)(sent / VS_CHUNK + 1u);             /* сколько порций после SM_CANCEL */
    }
    (void)vs_soft_reset(vs);                                /* не снялся за 2048 байт */
    return VS_E_CANCEL;
}

int vs_cancel(vs_t *vs, vs_ring_t *r)
{
    uint8_t f[VS_CHUNK];
    uint8_t efb = (uint8_t)vs_wram_read(vs, VS_PAR_END_FILL);
    uint32_t pos, end = r->head;
    int rc;
    r->eof = 1;
    r->head = r->tail + r->inflight;                        /* vs_feed больше ничего не возьмёт */
    while (r->inflight)
        vs_port_kick();
    pos = r->tail;
    for (uint32_t i = 0; i < VS_CHUNK; i++)
        f[i] = efb;
    if ((rc = vs_sci_write(vs, VS_MODE, (uint16_t)(vs->mode | SM_CANCEL))) != VS_OK)
        return rc;
    for (uint32_t sent = 0; sent < VS_CANCEL_MAX; sent += VS_CHUNK) {
        const uint8_t *p = f;
        if (end - pos >= VS_CHUNK) {                        /* сначала — файл из кольца, потом endFillByte */
            p = r->buf + (pos & (r->size - 1u));
            pos += VS_CHUNK;
        }
        if ((rc = sdi_send(vs, p, VS_CHUNK)) != VS_OK)
            return rc;
        if (!(vs_sci_read(vs, VS_MODE) & SM_CANCEL)) {      /* снялся: проверить, что заголовок обнулён */
            if (vs_sci_read(vs, VS_HDAT0) || vs_sci_read(vs, VS_HDAT1))
                break;
            return (int)(sent / VS_CHUNK + 1u);
        }
    }
    (void)vs_soft_reset(vs);                                /* не снялся за 2048 байт */
    return VS_E_CANCEL;
}

void vs_stream_bind(vs_t *vs)
{
    feed_vs = vs;
}

fw18i.c — прошивка F103: SPI1, DMA1 канал 3, EXTI1 на DREQ, FatFS, три трека по кругу.

/* fw18i.c — раздел 18з: MP3-плеер на VS1053b, STM32F103 (BluePill), 72 МГц, APB2 = 72 МГц.
 * SPI1 (режим 0): PA5 — SCK, PA6 — MISO, PA7 — MOSI; PA4 — xCS (SCI), PB0 — xDCS (SDI), PB1 — DREQ (вход,
 * прерывание EXTI1 по фронту), PB10 — XRESET. Посылки SDI по 32 байта — DMA1, канал 3 (SPI1_TX), его прерывание
 * и фронт DREQ зовут vs_feed. Файлы — FatFS с SD-карты (диск — раздел 42 и 18.10, здесь не повторяется).
 * Через отладчик: volume — ослабление в шагах 0,5 дБ, stop_req = 1 — остановить трек. Тактирование до 72 МГц —
 * как в разделе 6. */
#include "stm32f10x.h"
#include "ff.h"
#include "vs18i.h"

#define CPU_HZ          72000000u
#define XCS_PIN         4u              /* PA4 */
#define XDCS_PIN        0u              /* PB0 */
#define DREQ_PIN        1u              /* PB1 */
#define XRESET_PIN      10u             /* PB10 */

static vs_t vs;
static vs_ring_t ring;
static uint8_t ringbuf[4096];           /* 102,4 мс при 320 кбит/с сверх 2048 байт декодера */
static FATFS fs;
static FIL fil;
static volatile uint8_t sdi_active;
static uint8_t vol_set;
volatile uint8_t volume = 0x14, stop_req;
volatile int32_t vs_rc = 1, track_rc;
volatile uint32_t tracks_played;

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

void vs_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

static void hw_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_SPI1EN;
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    GPIOA->BSRR = 1u << XCS_PIN;                        /* xCS вверх до настройки вывода */
    GPIOB->BSRR = 1u << XDCS_PIN;
    /* PA4 — выход 50 МГц (0011), PA5 и PA7 — альтернативная функция (1011), PA6 — вход (0100) */
    GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B30000u;
    /* PB0 — выход (0011), PB1 — вход с подтяжкой вниз (1000, ODR = 0) */
    GPIOB->CRL = (GPIOB->CRL & 0xFFFFFF00u) | 0x00000083u;
    GPIOB->BRR = 1u << DREQ_PIN;
    GPIOB->CRH = (GPIOB->CRH & 0xFFFFF0FFu) | 0x00000300u;   /* PB10 — выход */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR;   /* режим 0, делитель 256 до vs_init */
    SPI1->CR1 |= SPI_CR1_SPE;
    DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
    AFIO->EXTICR[0] = (AFIO->EXTICR[0] & ~AFIO_EXTICR1_EXTI1) | AFIO_EXTICR1_EXTI1_PB;
    EXTI->RTSR |= 1u << DREQ_PIN;                       /* DREQ: 0 → 1 — декодер готов принять */
    EXTI->IMR |= 1u << DREQ_PIN;
    NVIC_EnableIRQ(EXTI1_IRQn);
    NVIC_EnableIRQ(DMA1_Channel3_IRQn);
}

/* ── порт драйвера ────────────────────────────────────────────────────── */
void vs_port_spi_hz(uint32_t hz, uint32_t br)
{
    (void)hz;
    while (SPI1->SR & SPI_SR_BSY) {
    }
    SPI1->CR1 &= ~SPI_CR1_SPE;                          /* BR меняют при выключенном SPI */
    SPI1->CR1 = (SPI1->CR1 & ~SPI_CR1_BR) | (br << 3);
    SPI1->CR1 |= SPI_CR1_SPE;
}

void vs_port_sci(const uint8_t tx[4], uint8_t rx[4])
{
    GPIOA->BRR = 1u << XCS_PIN;
    for (int i = 0; i < 4; i++) {
        while (!(SPI1->SR & SPI_SR_TXE)) {
        }
        SPI1->DR = tx[i];
        while (!(SPI1->SR & SPI_SR_RXNE)) {
        }
        rx[i] = (uint8_t)SPI1->DR;
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    GPIOA->BSRR = 1u << XCS_PIN;
}

void vs_port_sdi_start(const uint8_t *p, uint32_t n)
{
    GPIOB->BRR = 1u << XDCS_PIN;
    sdi_active = 1;
    DMA1_Channel3->CCR = 0;
    DMA1_Channel3->CMAR = (uint32_t)p;
    DMA1_Channel3->CNDTR = n;
    DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR | DMA_CCR3_TCIE | DMA_CCR3_EN;   /* память → SPI1->DR */
    SPI1->CR2 |= SPI_CR2_TXDMAEN;
}

static void sdi_done(void)
{
    if (!sdi_active || !(DMA1->ISR & DMA_ISR_TCIF3))
        return;
    while (!(SPI1->SR & SPI_SR_TXE) || (SPI1->SR & SPI_SR_BSY)) {
    }                                                   /* последний байт ушёл целиком */
    GPIOB->BSRR = 1u << XDCS_PIN;
    DMA1->IFCR = DMA_IFCR_CTCIF3;
    DMA1_Channel3->CCR = 0;
    SPI1->CR2 &= ~SPI_CR2_TXDMAEN;
    (void)SPI1->DR;                                     /* принятое не нужно: сбросить RXNE и OVR */
    (void)SPI1->SR;
    sdi_active = 0;
}

int vs_port_sdi_busy(void)
{
    sdi_done();
    return sdi_active;
}

int vs_port_dreq(void)
{
    return (GPIOB->IDR >> DREQ_PIN) & 1u;
}

void vs_port_xreset(int level)
{
    if (level)
        GPIOB->BSRR = 1u << XRESET_PIN;
    else
        GPIOB->BRR = 1u << XRESET_PIN;
}

void vs_port_bus(int take)
{
    if (take) {                                         /* SCI: кормление по DMA подождёт */
        NVIC_DisableIRQ(EXTI1_IRQn);
        NVIC_DisableIRQ(DMA1_Channel3_IRQn);
        while (vs_port_sdi_busy()) {
        }
    } else {
        NVIC_EnableIRQ(EXTI1_IRQn);
        NVIC_EnableIRQ(DMA1_Channel3_IRQn);
    }
}

void vs_port_kick(void)
{
    NVIC_SetPendingIRQ(EXTI1_IRQn);                     /* vs_feed — только в прерывании, без гонок */
}

void vs_port_idle(void)
{
    __WFI();
}

void EXTI1_IRQHandler(void)
{
    EXTI->PR = 1u << DREQ_PIN;
    vs_feed(&ring);
}

void DMA1_Channel3_IRQHandler(void)
{
    sdi_done();
    vs_feed(&ring);
}

static int sd_read(void *ctx, uint8_t *dst, uint32_t n)
{
    UINT br;
    return f_read((FIL *)ctx, dst, n, &br) == FR_OK ? (int)br : -1;
}

static int play(const char *name)
{
    int rc;
    if (f_open(&fil, name, FA_READ) != FR_OK)
        return VS_E_READ;
    vs_ring_init(&ring, ringbuf, sizeof ringbuf);
    while ((rc = vs_pump(&ring, sd_read, &fil)) == 1) {
        if (stop_req) {
            stop_req = 0;
            rc = vs_cancel(&vs, &ring);                 /* посреди трека: SM_CANCEL */
            break;
        }
        if (volume != vol_set) {                        /* громкость сменили через отладчик */
            vol_set = volume;
            (void)vs_set_volume(&vs, vol_set, vol_set);
        }
    }
    if (rc == 0)
        rc = vs_finish(&vs);                            /* конец файла: endFillByte и SM_CANCEL */
    f_close(&fil);
    return rc;
}

int main(void)
{
    static const char *const list[] = {"01.mp3", "02.mp3", "03.mp3"};
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    hw_init();
    vs_stream_bind(&vs);
    while ((vs_rc = vs_init(&vs, CPU_HZ, VS_FIX_GPIO_BOOT)) != VS_OK)
        vs_port_delay_us(100000u);                      /* нет ответа: питание, XRESET, xCS, MISO */
    vol_set = volume;
    (void)vs_set_volume(&vs, vol_set, vol_set);
    while (f_mount(&fs, "", 1) != FR_OK)
        vs_port_delay_us(500000u);                      /* нет карты */
    for (;;) {
        for (unsigned i = 0; i < sizeof list / sizeof list[0]; i++) {
            track_rc = play(list[i]);
            tracks_played++;
        }
    }
}

Дальше: раздел 19 «Шина CAN» — интерфейс для связи между платами и узлами.

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