Repository navigation
Communication SPI
SPI is a fast synchronous bus with a separate clock line. With SPIAudioMaster and SPIAudioSlave you can send audio from one microcontroller to another at several Mbit/s - e.g. from a main controller to a separate audio processor. In contrast to a serial link the protocol has flow control built in: the master asks the slave how much space is left in its buffer. See Communication for the general considerations.
Both classes are in AudioTools/Communication/SPI/SPIAudio.h. They are supported on the ESP32. A register based variant for more platforms is described below.
The master starts all transactions: a request is cmd(1) + payload_len(2) + payload, the slave answers with status(1) + response_len(2) + response. The commands are: set the MIME type, set the AudioInfo, write data, get the available / filled buffer size and clear the buffer.
SPIAudioMaster is an AudioOutput: write the audio to it.
#include "AudioTools.h"
#include "AudioTools/Communication/SPI/SPIAudio.h"
AudioInfo info(44100, 2, 16);
SineGenerator<int16_t> sineWave(32000);
GeneratedSoundStream<int16_t> sound(sineWave);
SPIAudioMaster master(SPI);
StreamCopy copier(master, sound);
void setup() {
SPI.begin();
SPIAudioMasterConfig cfg;
cfg.copyFrom(info);
cfg.cs_pin = SS;
cfg.clock_hz = 10000000;
cfg.is_blocking_write = true; // wait until the slave has space
master.begin(cfg);
master.setAudioInfo(info); // tell the slave the format
sineWave.begin(info, N_B4);
}
void loop() { copier.copy(); }Field (SPIAudioMasterConfig) |
Default | Description |
|---|---|---|
cs_pin |
SS | chip select (-1: handled by the hardware) |
clock_hz |
8000000 | SPI clock |
bit_order, data_mode
|
MSBFIRST, SPI_MODE0 | SPI settings |
max_payload |
1024 | max bytes per write command |
is_blocking_write |
false | false: write() only sends what fits into the slave's buffer; true: waits for space |
With is_blocking_write = true the master is slowed down to the speed at which the slave consumes the data - so a generator or file needs no Throttle.
SPIAudioSlave is an AudioStream: it writes the received data into a buffer that you provide, and you read it from there. On the ESP32 it uses the hardware SPI slave driver: each call of process() handles one byte of the SPI transfer, so call it continuously in a separate task:
#include "AudioTools.h"
#include "AudioTools/Communication/SPI/SPIAudio.h"
#include "AudioTools/Concurrency/RTOS.h"
AudioInfo info(44100, 2, 16);
BufferRTOS<uint8_t> buffer(10 * 1024); // thread-safe: written and read in different tasks
SPIAudioSlave slave(buffer);
I2SStream out;
StreamCopy copier(out, slave);
Task spi_task("spi", 4096, 10, 0);
void setup() {
SPIAudioSlaveConfig cfg;
cfg.copyFrom(info);
cfg.pin_mosi = 23;
cfg.pin_miso = 19;
cfg.pin_sclk = 18;
cfg.pin_cs = 5;
slave.addNotifyAudioChange(out); // follow the format sent by the master
slave.begin(cfg);
auto icfg = out.defaultConfig(TX_MODE);
icfg.copyFrom(info);
out.begin(icfg);
spi_task.begin([]() { slave.process(); });
}
void loop() { copier.copy(); }The AudioInfo and the MIME type (mimeType()) sent by the master are available on the slave. There is no example in the library yet, so treat the sketches above as a starting point.
Field (SPIAudioSlaveConfig) |
Default | Description |
|---|---|---|
pin_mosi, pin_miso, pin_sclk, pin_cs
|
23, 19, 18, 5 | the pins |
host, dma_chan
|
SPI2_HOST, 1 | the SPI peripheral and DMA channel |
queue_size |
4 | SPI transaction queue: increase for bursts |
max_command_payload |
1024 | must be at least the max_payload of the master |
use_hw_slave |
true | use the ESP32 hardware slave driver |
SPIRegAudioMaster and SPIRegAudioSlave are an alternative that works on more platforms: the slave is based on the TinySPISlave library, which supports the ESP32, RP2040 / RP2350 and STM32. Include AudioTools/Communication/SPI/SPIRegAudio.h. Instead of commands the slave offers a small register map:
Dependencies
-
Slave: install TinySPISlave (ESP32, RP2040 / RP2350 with the arduino-pico core, STM32). Add
#include "TinySPISlave.h"to your sketch beforeSPIRegAudio.h: this makes the Arduino build system find the library, andSPIRegAudioSlaveis only available if it is installed. -
Master: only the Arduino
SPIlibrary, any board with an SPI master works. TinySPISlave is not needed. -
Decoding on the slave (the example below): arduino-libhelix for
MP3DecoderHelixandAACDecoderHelix;WAVDecoderis part of AudioTools.
| Address | Register | Access | Content |
|---|---|---|---|
| 0x01 | SPIREG_AUDIO_INFO |
rw | sample rate (u32), channels (u16), bits per sample (u8), reserved - 8 bytes, little endian |
| 0x02 | SPIREG_MIME |
rw | zero terminated MIME type, 32 bytes |
| 0x03 | SPIREG_AVAILABLE |
ro | u32: free space in the slave's buffer, used for the flow control |
| 0x10 | SPIREG_DATA |
wo | the audio data: the bytes of the frame are appended to the slave's buffer |
The command byte is the register address (bit 7 = read). A write is one frame [cmd][bytes...]. A read is a request frame [cmd][n], a short pause with CS high and a second frame in which the master clocks n bytes. Address 0 is reserved for these dummy bytes. The master sets the AudioInfo and the MIME type, reads the available space and writes only as much data as fits.
Master
#include "AudioTools.h"
#include "AudioTools/Communication/SPI/SPIRegAudio.h"
AudioInfo info(44100, 2, 16);
SineWaveGenerator<int16_t> sineWave(16000);
GeneratedSoundStream<int16_t> sound(sineWave);
SPIRegAudioMaster master;
WAVEncoder wav;
EncodedAudioOutput enc(&master, &wav);
StreamCopy copier(enc, sound);
void setup() {
auto cfg = SPIRegAudioMasterConfig();
cfg.copyFrom(info);
cfg.cs_pin = 5;
cfg.is_blocking_write = true; // the encoder can not repeat a write
master.begin(cfg); // defines the AudioInfo on the slave
master.setMime(wav.mime()); // "audio/wav"
sineWave.begin(info, N_B4);
enc.begin(info);
}
void loop() { copier.copy(); }Field (SPIRegAudioMasterConfig) |
Default | Description |
|---|---|---|
cs_pin |
SS | chip select (-1: handled by the hardware) |
clock_hz |
4000000 | SPI clock |
bit_order, data_mode
|
MSBFIRST, SPI_MODE0 | SPI settings |
max_payload |
256 | max audio bytes per frame, must not be bigger than the slave's |
reply_gap_us |
300 | pause between a read request and the reply frame |
is_blocking_write |
false | false: write() only sends what fits; true: waits for space |
Slave
The slave is an AudioStream and also a MimeSource: a MultiDecoder can select the decoder from the MIME type sent by the master. For audio/pcm the decoder is bypassed and the AudioInfo register defines the output format.
#include "TinySPISlave.h" // must be included before SPIRegAudio.h
#include "AudioTools.h"
#include "AudioTools/AudioCodecs/CodecHelix.h"
#include "AudioTools/Communication/SPI/SPIRegAudio.h"
RingBuffer<uint8_t> buffer(16 * 1024);
SPIRegAudioSlave slave(buffer);
I2SStream out;
MP3DecoderHelix mp3;
AACDecoderHelix aac;
WAVDecoder wav;
MultiDecoder multi(slave); // decoder is selected by the mime of the slave
EncodedAudioOutput dec(&out, &multi); // the decoders set the format of out
StreamCopy copier(dec, slave); // encoded data
StreamCopy pcm_copier(out, slave); // audio/pcm: directly to the output
bool is_pcm = false;
// A new mime type: restart the decoder so that it gets selected again
void onMime(const char* mime, void*) {
is_pcm = StrView(mime).equalsIgnoreCase("audio/pcm");
dec.end();
dec.begin();
}
void setup() {
multi.addDecoder(mp3, "audio/mpeg");
multi.addDecoder(aac, "audio/aac");
multi.addDecoder(wav, "audio/wav");
out.begin(out.defaultConfig(TX_MODE));
auto cfg = SPIRegAudioSlaveConfig();
cfg.sck = 18; cfg.miso = 19; cfg.mosi = 23; cfg.cs = 5;
slave.addNotifyAudioChange(out); // AudioInfo register -> output (pcm)
slave.setMimeCallback(onMime);
slave.begin(cfg);
dec.begin();
}
void loop() {
slave.update(); // applies AudioInfo / mime changes
if (is_pcm) pcm_copier.copy(); else copier.copy();
}Field (SPIRegAudioSlaveConfig) |
Default | Description |
|---|---|---|
sck, miso, mosi, cs
|
-1 | the pins (RP2040: fixed by the function select, see TinySPISlave) |
spi_mode |
0 | SPI mode 0-3 |
max_payload |
256 | max audio bytes per frame |
spi_host |
2 | ESP32 only: SPI2 or SPI3 |
- The SPI frames are processed in an interrupt (RP2040 / STM32) or in a task (ESP32): use a buffer for one writer and one reader, e.g.
RingBuffer. Bytes which are sent despite a full buffer are dropped and counted inoverflowBytes(). - Call
update()fromloop(): it applies the AudioInfo / MIME changes in the application context. - Examples:
examples-communication/spi/spi-reg-masterandspi-reg-slave. They have only been compiled so far (ESP32, RP2040), not tested on hardware.
- Keep the wires short and connect GND; reduce
clock_hzif you get errors. - The slave's buffer determines the latency: big enough to cover the jitter, small enough for the delay you accept.