Why is the I2S DMA buffer full after setup?
Posted: Thu Apr 17, 2025 12:37 pm
I have run an experiment. It measures how long it takes to call i2s_write in a loop and then logs the result.
The output looks like this:
The numbers are microseconds it takes to wait for the i2s_write call to finish. DMA_BUF_LEN is 1000 samples and the chunks of data that are written are 100 samples. So as expected, it is waiting for a buffer to be free and then writes ten blocks in quick succession. When the CPU is busy for some time (less than the duration of DMA_BUF_COUNT full dma buffer writes), it can write longer bursts to fill up the empty buffers again, as demonstrated with the 300ms delay. If there is longer delay than it can handle, as demonstrated with the 1s wait, it will only fill as many buffers as it has afterwards (minus the one it is currently reading from I guess).
But why is there no long burst in the beginning? Are all buffers full with something after initialization? I would expect the buffers to be empty after i2s_driver_install and swallow a large burst of data... Does this mean that if my DMA_BUF_COUNT is 100, I have to wait for 100 buffer fills worth of time until my actual data starts playing?
EDIT: I have modified the code to use the new i2s driver, it still shows the same behavior:
Code: Select all
#include <driver/i2s.h>
#define I2S_BCLK 15
#define I2S_LRC 16
#define I2S_DOUT 17
#define SAMPLE_RATE 10000
#define I2S_NUM I2S_NUM_0
#define DMA_BUF_LEN 1000
#define DMA_BUF_COUNT 5
#define MEM_BUF_LEN 100
#define NUM_WRITES 200
int16_t audioBuffer[MEM_BUF_LEN];
size_t logs[NUM_WRITES];
void setup() {
Serial.begin(115200);
for (int i = 0; i < MEM_BUF_LEN; i++) {
audioBuffer[i] = 0;
}
i2s_config_t i2s_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = DMA_BUF_COUNT,
.dma_buf_len = DMA_BUF_LEN};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRC, .data_out_num = I2S_DOUT, .data_in_num = I2S_PIN_NO_CHANGE};
i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM, &pin_config);
size_t bytesWritten;
size_t tLast = micros();
for (size_t k = 0; k < NUM_WRITES; k++) {
// artificial lag
if (k == 50) { // short lag, 3 dma buffers worth of data
delayMicroseconds(300000);
}
if (k == 120) { // long lag, almost 10 dma buffers worth of data (we only have 5)
delayMicroseconds(1000000);
}
i2s_write(I2S_NUM, audioBuffer, sizeof(audioBuffer), &bytesWritten, portMAX_DELAY);
size_t t = micros();
logs[k] = t - tLast;
tLast = t;
}
Serial.print("\n\n\n");
for (size_t i = 0; i < NUM_WRITES; i++) {
Serial.print(logs[i]);
Serial.print(", ");
if (i % 10 == 9) {
Serial.println("");
}
}
}
void loop() {}
The output looks like this:
Code: Select all
96359, 10, 7, 7, 6, 7, 6, 7, 7, 6,
99933, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
300011, 7, 6, 7, 7, 6, 7, 6, 7, 7,
8, 6, 7, 7, 6, 7, 6, 7, 7, 6,
9, 6, 8, 9, 6, 7, 7, 6, 7, 6,
99732, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
1000009, 6, 7, 7, 6, 7, 6, 7, 7, 6,
9, 6, 7, 6, 7, 7, 6, 7, 6, 7,
8, 7, 7, 9, 7, 6, 7, 6, 7, 7,
8, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99667, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
99941, 6, 7, 7, 6, 7, 6, 7, 7, 6,
The numbers are microseconds it takes to wait for the i2s_write call to finish. DMA_BUF_LEN is 1000 samples and the chunks of data that are written are 100 samples. So as expected, it is waiting for a buffer to be free and then writes ten blocks in quick succession. When the CPU is busy for some time (less than the duration of DMA_BUF_COUNT full dma buffer writes), it can write longer bursts to fill up the empty buffers again, as demonstrated with the 300ms delay. If there is longer delay than it can handle, as demonstrated with the 1s wait, it will only fill as many buffers as it has afterwards (minus the one it is currently reading from I guess).
But why is there no long burst in the beginning? Are all buffers full with something after initialization? I would expect the buffers to be empty after i2s_driver_install and swallow a large burst of data... Does this mean that if my DMA_BUF_COUNT is 100, I have to wait for 100 buffer fills worth of time until my actual data starts playing?
EDIT: I have modified the code to use the new i2s driver, it still shows the same behavior:
Code: Select all
#include "driver/i2s_std.h"
#define I2S_BCLK GPIO_NUM_15
#define I2S_LRC GPIO_NUM_16
#define I2S_DOUT GPIO_NUM_17
#define SAMPLE_RATE 10000
#define DMA_BUF_LEN 1000
#define DMA_BUF_COUNT 5
#define MEM_BUF_LEN 100
#define NUM_WRITES 200
int16_t audioBuffer[MEM_BUF_LEN];
size_t logs[NUM_WRITES];
i2s_chan_handle_t tx_handle;
void setup() {
Serial.begin(115200);
delay(1000);
for (int i = 0; i < MEM_BUF_LEN; i++) {
audioBuffer[i] = 0;
}
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = DMA_BUF_COUNT,
.dma_frame_num = DMA_BUF_LEN,
.auto_clear = true
};
i2s_new_channel(&chan_cfg, &tx_handle, NULL);
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCLK,
.ws = I2S_LRC,
.dout = I2S_DOUT,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
i2s_channel_init_std_mode(tx_handle, &std_cfg);
i2s_channel_enable(tx_handle);
size_t bytesWritten;
size_t tLast = micros();
for (size_t k = 0; k < NUM_WRITES; k++) {
if (k == 50) delayMicroseconds(300000);
if (k == 120) delayMicroseconds(1000000);
i2s_channel_write(tx_handle, audioBuffer, sizeof(audioBuffer), &bytesWritten, portMAX_DELAY);
size_t t = micros();
logs[k] = t - tLast;
tLast = t;
}
Serial.print("\n\n\n");#include "driver/i2s_std.h"
#define I2S_BCLK GPIO_NUM_15
#define I2S_LRC GPIO_NUM_16
#define I2S_DOUT GPIO_NUM_17
#define SAMPLE_RATE 10000
#define DMA_BUF_LEN 1000
#define DMA_BUF_COUNT 5
#define MEM_BUF_LEN 100
#define NUM_WRITES 200
int16_t audioBuffer[MEM_BUF_LEN];
size_t logs[NUM_WRITES];
i2s_chan_handle_t tx_handle;
void setup() {
Serial.begin(115200);
delay(1000);
for (int i = 0; i < MEM_BUF_LEN; i++) {
audioBuffer[i] = 0;
}
i2s_chan_config_t chan_cfg = {
.id = I2S_NUM_0,
.role = I2S_ROLE_MASTER,
.dma_desc_num = DMA_BUF_COUNT,
.dma_frame_num = DMA_BUF_LEN,
.auto_clear = true
};
i2s_new_channel(&chan_cfg, &tx_handle, NULL);
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = I2S_BCLK,
.ws = I2S_LRC,
.dout = I2S_DOUT,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false
}
}
};
i2s_channel_init_std_mode(tx_handle, &std_cfg);
i2s_channel_enable(tx_handle);
size_t bytesWritten;
size_t tLast = micros();
for (size_t k = 0; k < NUM_WRITES; k++) {
if (k == 50) delayMicroseconds(300000);
if (k == 120) delayMicroseconds(1000000);
i2s_channel_write(tx_handle, audioBuffer, sizeof(audioBuffer), &bytesWritten, portMAX_DELAY);
size_t t = micros();
logs[k] = t - tLast;
tLast = t;
}
Serial.print("\n\n\n");
for (size_t i = 0; i < NUM_WRITES; i++) {
Serial.print(logs[i]);
Serial.print(", ");
if (i % 10 == 9) Serial.println("");
}
}
void loop() {}
for (size_t i = 0; i < NUM_WRITES; i++) {
Serial.print(logs[i]);
Serial.print(", ");
if (i % 10 == 9) Serial.println("");
}
}
void loop() {}