Embedded HandbookApps
All sections › III. Peripherals
STM32F103 · level intermediate · 44 min read
Open in the interactive handbook

18з. VS1053 MP3 decoder over SPI: SCI and SDI, DREQ, streaming from SD via DMA, volume, reset and patches

In 16г the sound came from a ready-made DFPlayer module: card, decoder and amplifier inside, and the microcontroller only sends "play file 3" over UART. In 18а the sound went out over I2S: the STM32 computes the samples itself and the codec only outputs them. The VS1053b sits in between. It is a decoder for MP3, WAV, MIDI and other formats with a DAC and a headphone output, but the file is handed to it by the microcontroller — byte after byte over SPI, from its own card, from the network, from anywhere. This section is about the driver: two buses on one SPI, the DREQ pin, the SPI speed before and after the clock is set up, reset, volume, reading the format and bit rate, a stream from an SD card through a ring and DMA so that card latency does not break the sound, stopping a track properly and loading patches.

Sources: RM0008 (DocID13902 Rev 17): SPI1 without remap — PA4–PA7, p. 185; SPI1_TX — DMA1, channel 3, table 78, p. 281; bits MINC, p. 285, and DIR, p. 286; SSM and SSI, p. 743; BR[2:0], p. 743; CPOL and CPHA, p. 744; TXDMAEN and the BSY flag, p. 745; flags TXE and RXNE, p. 746. DocID13587 Rev 17 (STM32F103x8/xB): PA4–PA7 — not 5 V tolerant, p. 29. How SPI and DMA work is in sections 18 and 9, the file system in 42, the card over SPI in 18.10. There is no manufacturer document on the VS1053b in the project, and no driver on disk either. The SCI commands, register numbers and bits, addresses in the decoder's memory, the SPI speed limits, the 2048-byte buffer, the meaning of DREQ, the stopping procedure and the patch format are taken from the manufacturer's documentation and not verified; they are checked only against our own model of the chip, which was built from the same information. All numbers below come from running the driver _verify/mp3_18i/vs18i.c, built with arm-none-eabi-gcc 12.2.1 for Cortex-M3 and run in QEMU 7.2.22, against a model of the VS1053b, the SD card and the DMA in Python 3.11; the firmware is built with armclang 6.24 for Cortex-M3. SD card latencies, the time after a register write and the behaviour with a too fast SPI are model assumptions: these numbers describe the model, not the chip.

What it is called in the documentation

Original Meaning
SCI — serial control interface command bus: 16-bit registers, select xCS
SDI — serial data interface data bus: the file's bytes, select xDCS
DREQ — data request pin "will take 32 more bytes"
XTALI, CLKI the decoder's crystal and its core clock after the multiplier
clock multiplier (SC_MULT) clock multiplier in the CLOCKF register
software reset (SM_RESET) reset by a bit in MODE
new SDI mode (SM_SDINEW) "new" mode: data has its own select xDCS
cancel playback (SM_CANCEL) stop decoding and discard the buffer
endFillByte filler byte after the end of a file
stream header (HDAT0, HDAT1) stream header: format and bit rate
sine test (SM_TESTS) sine test: a tone without a file
patch, plugin (VLSI) patch: code loaded into the decoder's memory over SCI

Top — the VS1053b module connected to the STM32F103 over SPI1 in mode 0: PA5 — SCK, PA6 — MISO, PA7 — MOSI, PA4 — xCS for the SCI command bus, PB0 — xDCS for the SDI data bus, PB1 — the DREQ input, which is 1 when at least 32 bytes are free in the decoder's buffer, PB10 — XRESET; a 12.288 MHz crystal on the module; an SCI command is 4 bytes: 02 write or 03 read, the address and 16 bits most significant byte first; before CLOCKF is written, writes go at 2.25 MHz and reads at 1.125 MHz, after CLOCKF = 0x8800 and a 43.008 MHz core clock — writes and data at 9 MHz, reads at 4.5 MHz; volume VOL — 0.5 dB steps, 0xFE — silence. Middle — the stream path: an SD card with FatFS and 512-byte sectors, a 4096-byte ring in RAM written by the main loop, DMA1 channel 3 in 32-byte portions on xDCS, the decoder's 2048-byte buffer, the decoder with its DAC; the DREQ rising edge through EXTI1 starts the next portion; 128 kbit/s is 16 000 bytes/s, the decoder's buffer lasts 128 ms, 320 kbit/s is 40 000 bytes/s, 51.2 ms. Bottom — stopping a track: 2052 bytes of endFillByte from WRAM 0x1E06, then SM_CANCEL and 32-byte portions until the bit clears, not cleared within 2048 bytes — software reset; MODE bits: SM_SDINEW 0x0800, SM_RESET 0x0004, SM_CANCEL 0x0008, SM_TESTS 0x0020; MP3 header: HDAT1 = 0xFFFB — MPEG1 Layer III, HDAT0 = 0x9244 — 128 kbit/s, 44.1 kHz


18з.1 Structure: two buses on one SPI, DREQ and the registers

The VS1053b has one SPI and two selects (manufacturer's documentation, not verified):

Hence the driver's rule: data goes in 32-byte portions and only when DREQ = 1; before an SCI write, wait for DREQ as well.

register number contents
MODE 0 modes: SM_RESET 0x0004, SM_CANCEL 0x0008, SM_TESTS 0x0020, SM_SDINEW 0x0800; 0x4800 after reset
STATUS 1 bits 7:4 — version, 4 for the VS1053
CLOCKF 3 SC_MULT (15:13) — crystal multiplier, SC_ADD (12:11), SC_FREQ (10:0) — non-standard crystal
DECODE_TIME 4 seconds since decoding started
AUDATA 5 sample rate (bits 15:1), bit 0 — stereo
WRAM, WRAMADDR 6, 7 window into the decoder's memory: address, then words, the address increments
HDAT0, HDAT1 8, 9 stream header: for MP3 — the first 4 bytes of a frame
AIADDR 10 start address of a patch
VOL 11 attenuation: high byte — left channel, low byte — right

The model is the VS1053b as an SPI slave: registers, a 2048-byte buffer, DREQ, parsing of real MP3 frame headers with their bit rate and duration, WAV and MIDI by header, cancel, reset, the sine test, the decoder's memory. It builds the MP3 files for the run itself: MPEG1 Layer III, 44.1 kHz, joint stereo, real headers and frame lengths, and bytes 1…254 instead of sound. The model makes no sound: to "play" means taking frames out of the buffer at their own pace.


18з.2 Connecting to the F103, reset and start-up: SPI speed before and after CLOCKF

SPI1 without remap is PA4–PA7 (p. 185), all four are not 5 V tolerant (p. 29), so the module is powered from 3.3 V. PA5 — SCK, PA6 — MISO, PA7 — MOSI. The selects xCS and xDCS are plain outputs: PA4 and PB0, SSM = SSI = 1 (p. 743). DREQ is PB1, an input with an EXTI1 interrupt on the rising edge, XRESET is PB10. SPI mode 0: CPOL = CPHA = 0 (p. 744).

Speed. After reset the decoder's core clock CLKI equals the crystal XTALI, 12.288 MHz. SCI and SDI writes — no faster than CLKI/4, SCI reads — no faster than CLKI/7 (manufacturer's documentation, not verified). CLOCKF = 0x8800 — multiplier 3.5 and +1.0 of headroom for heavy formats (documentation, not verified) — raises CLKI to 43.008 MHz. The SPI1 divider is a power of two from 72 MHz, and no faster than 18 MHz (18.3). The driver picks the fastest one that does not exceed the limit:

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 write: limit SPI1 read: limit SPI1
before CLOCKF 12.288 MHz 3.072 MHz 2.25 MHz, BR = 4 1.7554 MHz 1.125 MHz, BR = 5
after CLOCKF 43.008 MHz 10.752 MHz 9 MHz, BR = 2 6.144 MHz 4.5 MHz, BR = 3

An SCI command is 32 bits: 14.22 µs at 2.25 MHz and 3.56 µs at 9 MHz.

Start-up. Hardware reset with XRESET, wait for DREQ, check the version, MODE with SM_SDINEW, CLOCKF, and read both back:

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;
}

The start-up exchanges in the model:

SCI command reply SPI what
03 01 00 00 0040 1.125 MHz STATUS: version 4
02 00 08 00 — 2.25 MHz MODE = SM_SDINEW
02 03 88 00 — 2.25 MHz CLOCKF = 0x8800
03 00 00 00 0800 4.5 MHz MODE read back
03 03 00 00 8800 4.5 MHz CLOCKF read back

The whole start-up takes 2988.1 µs: XRESET is low for 1 ms, the rest is DREQ after reset — about 22 000 XTALI cycles, 1790.4 µs (manufacturer's documentation, not verified; so in the model). The driver waited for DREQ 173 times, 10 µs each. No module — in the model MISO floats high, STATUS reads 0xFFFF, version 15: VS_E_ID, −2. DREQ never rises — after 100 ms VS_E_TIMEOUT, −1 (in the model after 111.0 ms including the polls).

Reset. Hardware — XRESET low and high; in the model it took 2794.0 µs and returned CLOCKF to zero. Software — a MODE write with 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);
}

In the model — 1819.8 µs, after it MODE = 0x0800 and CLOCKF = 0x8800. SM_SDINEW is written together with SM_RESET: 0x0804, not 0x0004 (18з.7). After a software reset the model returns CLOCKF to zero (a model assumption), so the driver lowers the SPI again and rewrites CLOCKF — this is right whatever the chip does.

Module and power. Common VS1053b boards carry a 12.288 MHz crystal, regulators for 3.3 V and 1.8 V — the chip's core runs from 1.8 V, the analogue part and the pins from 3.3 V, — a headphone jack and sometimes a microSD slot on the same SPI (manufacturer's documentation and module descriptions, not verified). The common wire of the headphone output is not ground but the GBUF pin at half the supply: it must not be tied to an amplifier's ground (documentation, not verified). The model does not compute voltages.


18з.3 Volume, format, bit rate and the sine test

Volume. VOL is attenuation from maximum in 0.5 dB steps, the high byte for the left channel, the low byte for the right; 0xFE is silence (manufacturer's documentation, not verified):

vs_set_volume VOL left right
0x00, 0x00 0x0000 0 dB 0 dB
0x14, 0x14 0x1414 −10 dB −10 dB
0x28, 0x28 0x2828 −20 dB −20 dB
0x00, 0xFE 0x00FE 0 dB −127 dB, silence
0xFE, 0xFE 0xFEFE silence silence

Format and bit rate. HDAT1 says what the stream is: for MP3 it holds the sync word — eleven ones, the version and the layer. HDAT0 is the second word of the frame header: the bit rate index in bits 15:12, the sample rate index in 11:10. AUDATA is the sample rate and the stereo bit, DECODE_TIME — seconds:

    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;

The model in the middle of a track:

stream HDAT1 HDAT0 AUDATA the driver read
MP3 128 kbit/s 0xFFFB 0x9244 0xAC45 MPEG1, Layer III, 128 kbit/s, 44 100 Hz, stereo
MP3 320 kbit/s 0xFFFB 0xE244 0xAC45 MPEG1, Layer III, 320 kbit/s, 44 100 Hz, stereo
WAV 8 kHz, 16 bit, mono 0x7665 — 0x1F40 WAV, 8000 Hz, mono
MIDI 0x4D54 — — MIDI

Index 9 is 128 kbit/s, 14 is 320 kbit/s (the MPEG1 Layer III table, documentation, not verified). In the middle of a 3 s file DECODE_TIME is 1. There is no "MP3 or WAV mode" to switch: the decoder recognises the format by the first bytes, and after SM_CANCEL (18з.6) a file of another format may follow at once. The model only recognises a MIDI file, it does not play it.

Sine test. With SM_TESTS set in MODE, eight bytes 53 EF 6E n 0 0 0 0 over SDI switch a tone on and 45 78 69 74 0 0 0 0 switch it off. The frequency is Fs · S / 128, where Fs is given by the three upper bits of n (44 100, 48 000, 32 000, 22 050, 24 000, 16 000, 11 025, 12 000 Hz) and S by the five lower ones (manufacturer's documentation, not verified):

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 Hz, n = 0x7E — 22 050 · 30 / 128 = 5167.97 Hz (the driver uses integers — 5167). The model switched the tone on at the same frequency and off again, MODE went back to 0x0800. Without SM_TESTS the same eight bytes are just data: no tone. The sine test checks the DAC, the amplifier and the headphones without a file and without an SD card — the first thing to run on a new board.


18з.4 Streaming from SD: ring, DMA and the DREQ interrupt

FatFS (section 42) reads the file in 512-byte sectors into a ring in RAM. vs_feed hands it to the decoder: while DREQ = 1 and the ring has 32 bytes, start a DMA portion. A portion is always contiguous: the ring size is a power of two, and both the sector and 32 divide it.

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);
}

The main loop is vs_pump: room for a sector — read the SD, otherwise wait:

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;
}

Reading an SD sector takes sometimes a fraction of a millisecond, sometimes tens of milliseconds (18з.5). If only the main loop calls vs_feed, the decoder gets no data for that time. So in the firmware it is called from interrupts: the DREQ rising edge (EXTI1) and the end of the DMA portion (channel 3, SPI1_TX — p. 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 — memory to peripheral, MINC — the memory address increments (p. 286 and 285), TXDMAEN — a request on TXE (p. 745). What arrives on MISO is not needed: at the end of a portion the driver reads DR and SR, as in 18.6. An SCI command from the main loop and a DMA portion share one SPI: for the SCI the port disables both interrupts and waits for the portion to end (vs_port_bus).

The run: a 3 s file, a 4096-byte ring, feeding from interrupts, an SD without long delays:

bit rate bytes frames 32-byte portions dropouts played SPI busy
128 kbit/s 48 065 115 1503 0 3004.082 ms 1.54 %
320 kbit/s 120 163 115 3756 0 3004.082 ms 3.74 %

An MP3 frame at 44.1 kHz is 1152 samples, 26.122 ms; at 128 kbit/s it has 417 or 418 bytes, at 320 — 1044 or 1045. A 32-byte portion at 9 MHz takes 28.44 µs, at 2.25 MHz — 113.78 µs: the bus is not the bottleneck, the SD is.


18з.5 How much buffer is needed: bit rate and SD delays

Bit rate is bits per second; in bytes it is 8 times less: 128 kbit/s is 16 000 bytes/s, 320 kbit/s is 40 000 bytes/s. The decoder's buffer, 2048 bytes, lasts 128 ms at 128 kbit/s and 51.2 ms at 320.

A card answers unevenly: usually a sector in a fraction of a millisecond, but sometimes in tens of milliseconds (in the model: 0.8 ms, and every 40th sector 100 ms more, a model assumption). While the card is silent, the sound comes from the buffers. 100 ms at 320 kbit/s needs 4000 bytes. The decoder's buffer gives 2048 — not enough. When reading in sectors the ring is at least its size minus 511 bytes full: 2048 + 2048 − 511 = 3585 — still not enough; 2048 + 4096 − 511 = 5633 — enough. At 128 kbit/s 100 ms is 1600 bytes, and the decoder's buffer alone is enough. The model, a 4 s file:

bit rate feeding ring 1024 2048 4096 8192
128 kbit/s, 3 delays from the loop 0 0 0 0
128 kbit/s, 3 delays interrupts 0 0 0 0
320 kbit/s, 7 delays from the loop 7, 353.151 ms 7, 353.183 ms 7, 353.151 ms 7, 354.984 ms
320 kbit/s, 7 delays interrupts 7, 270.582 ms 4, 92.187 ms 0 0

Each cell is how many times the decoder ran out of frames and how long the silence lasted. Feeding from the main loop at 320 kbit/s is not saved by any ring: while the loop sits in f_read, nobody hands the ring over. With interrupts 4096 bytes are enough — a fifth of the BluePill's RAM. The longer the card's delays, the bigger the ring; their length and frequency are worth measuring on your own card.


18з.6 Stopping a track: endFillByte, SM_CANCEL and patches

End of file. The decoder finishes the last frame only if more bytes follow it. So after the file 2052 bytes of endFillByte are sent — its value lives in the decoder's memory at 0x1E06, — then SM_CANCEL is set and 32 bytes are sent at a time until the bit clears; not cleared within 2048 bytes — software reset (manufacturer's documentation, not verified):

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;
}

In the model endFillByte is 0x00. After a 3-second track SM_CANCEL cleared after the very first portion, all 115 frames were played, 0 bytes left in the buffer, HDAT1 cleared.

In the middle of a track. vs_cancel stops the feeding, sets SM_CANCEL and sends the rest of the file from the ring, checking the bit after every portion; when it clears, HDAT0 and HDAT1 must be zero, otherwise — reset. In the model, a stop after 1 s: the bit cleared after 14 portions — the decoder discarded the buffer at the end of the frame being played, — 35 frames were played, HDAT0 = HDAT1 = 0.

Without it. The same track was simply abandoned — data stopped and the next file began at once. The model: after 300 ms 374 bytes were still lying in the buffer — an incomplete last frame waiting for the rest. The next file was appended to it: one frame made of two files came out, then 373 bytes of garbage up to the nearest header — the first frame of the second track was lost. A damaged frame is a click. With vs_cancel before the next file — 0 damaged frames.

Patches. VLSI publishes patches and plugins for the VS1053b — fixes and new features, for example decoders of other formats. This is code for the decoder's memory; it is loaded over SCI after every reset, because the memory is erased. The format is an array of 16-bit words: register number, count, values; the top bit of the count means repeat one value (manufacturer's documentation, not verified):

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;
}

There are no real VLSI patches on disk; the loader was checked with our own: address 0x8010 into WRAMADDR, four words in a row, three more as a single repeat, and the start AIADDR = 0x0050. The model received 9 SCI writes, the memory holds 1111, 2222, 3333, 4444, 00AB, 00AB, 00AB, the start address 0x0050. A truncated array — VS_E_ARG, −5.


18з.7 Pitfalls

Fast SPI before CLOCKF. 9 MHz straight after reset: in the model both writes — MODE and CLOCKF — were lost (a model assumption: a too fast write is lost), CLOCKF stayed 0x0000, and MODE read at 9 MHz gave 0x2400 instead of 0x4800 (assumption: a too fast read shifts the word). The same MODE at 1.125 MHz reads 0x4800. vs_init reads MODE and CLOCKF back: on such an error it returns VS_E_VERIFY, −3.

xCS and xDCS swapped. The volume write 02 0B 20 20 went to xDCS: VOL stayed 0x0000, and the four bytes landed in the stream buffer as data. The start of an MP3 frame FF FB 90 44 went to xCS: the decoder did not recognise a command and stayed silent, HDAT1 = 0x0000. No error, no sound.

Not waiting for DREQ. A naive loop: an SD sector — 16 portions in a row without checking DREQ. A 2 s file, 32 182 bytes; in the model 28 473 of them were lost on a full buffer, the decoder found 8 frames out of 77 and lost sync 3 times. DREQ is not a hint but a condition.

"Old" SDI mode. A software reset by writing 0x0004 cleared SM_SDINEW: MODE = 0x0000. Without it xDCS does not work (manufacturer's documentation; in the model bytes without SM_SDINEW are lost). A one-second file went nowhere: 0 frames, DECODE_TIME 0, SM_CANCEL did not clear within 2048 bytes, and vs_finish did a software reset, VS_E_CANCEL, −4. MODE is always written with SM_SDINEW: 0x0800, on reset — 0x0804.

The module starts in MIDI. On some modules the GPIO1 pin is pulled up, and the decoder wakes up in real-time MIDI mode: it takes MP3 bytes for notes (a known problem of such modules, not verified). In the model such a module played 0 frames out of 38. The fix is in vs_init with the VS_FIX_GPIO_BOOT flag: WRAM 0xC017 = 3 and 0xC019 = 0, i.e. GPIO0 and GPIO1 become outputs driving zero, then a software reset; after it — 38 frames out of 38.

A click on stop. A track abandoned without endFillByte and SM_CANCEL — the tail of a frame mixed with the next file (18з.6). Stop only through vs_finish or vs_cancel.

Feeding from the main loop. At 320 kbit/s any SD delay longer than 51.2 ms is silence, and the ring does not help (18з.5). Portions — from the DREQ and DMA interrupts.

SCI in the middle of a DMA portion. Reading HDAT or writing VOL during an SDI portion spoils both: the selects differ, but the SPI is one. In the driver the SCI takes the bus through vs_port_bus: the feeding interrupts are off, the DMA portion has finished.


18з.8 Summary

Full sources

vs18i.h — VS1053b registers and bits, structures, driver and platform interface.

/* 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 speed, reset and start-up, volume, format, sine test, patches, ring, DMA and stopping.

/* 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 firmware: SPI1, DMA1 channel 3, EXTI1 on DREQ, FatFS, three tracks in a loop.

/* 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++;
        }
    }
}

Next: section 19 "The CAN bus" — an interface for communication between boards and nodes.

Open in the interactive handbook