Hi (newbie here),
I'm working on a project that inputs audio packets via UDP (VBAN protocol) and outputs them to an external I2S DAC. It's working fine, other than occasional crackles.
I suspect that I'm suffering DMA buffer underrun, but don't know a way to detect when the I2S/DMA has run out of fresh data and is going to re-use old buffers.
Simply, my code is an interrupt driven routine to put incoming packets into a double-buffered queue.
udpIn.onPacket(UDPtoQ)
The I2S writing code, which I have tried as both an xTask and in the main program loop, refills the I2S DMA buffers (I've tried 2 - 6 * 256 sample buffers with no real difference) using i2s_write().
I can detect when the DMA queue was full, by measuring the time it takes to execute i2s_write().
Is there a way to detect when the DMA queue is empty? I've tried reading I2S0.state.tx_idle, I2S0.int_st.out_done, I2S0.int_st.tx_rempty, I2S0.int_st.out_eof and I2S0.int_raw.tx_rempty.
None of them seem to return useful data for this purpose - possibly because the DMA is continually re-using the old buffers.
Thanks in anticipation.
I2S - detect empty DMA queue
-
- Posts: 3
- Joined: Fri Mar 05, 2021 12:54 am
Re: I2S - detect empty DMA queue
Hi palmerr23, I'm a newbie too.
I'm having a similar problem, I want to trigger a wave sample every now and then. It's a random pulse generator. I want the I2S to stop whenever the buffer's empty. However, as you said, the I2S IDF driver hijacks the I2S interrupts to keep refilling the buffer. I tried to reallocate as a shared interrupt (ESP_INTR_FLAG_SHARED), i.e.
but it didn't work. So I don't think there's a way to listen to the I2S interrupts without rewriting a version the IDF I2S library itself. However the code seems complex, since it has to play nice with the RTOS functions. It would need some patience to do that.
I found an article that does it for the ADC:
https://www.toptal.com/embedded/esp32-audio-sampling
I'm having a similar problem, I want to trigger a wave sample every now and then. It's a random pulse generator. I want the I2S to stop whenever the buffer's empty. However, as you said, the I2S IDF driver hijacks the I2S interrupts to keep refilling the buffer. I tried to reallocate as a shared interrupt (ESP_INTR_FLAG_SHARED), i.e.
Code: Select all
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
int flags = ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_IRAM ;
esp_intr_alloc(ETS_I2S0_INTR_SOURCE, flags, i2sInterrupt, NULL, &my_interrupt_handle);
I found an article that does it for the ADC:
https://www.toptal.com/embedded/esp32-audio-sampling
Re: I2S - detect empty DMA queue
KA101,
I solved the problem in a different way.
When installing the I2S driver, there's an option to create an event queue, which flags TX_DONE (or RX_DONE).
I then created an xTask that sampled the queue, processing an I2S buffer each time the queue indicated one was completed.
To make sure I wasn't waiting for a buffer to clear, I measure the time it takes to write an I2S buffer. If it's longer than 50uS, then I've had to wait for I2S, so I skip the next TX_DONE flag.
It seems to work OK.
Hope this helps you.
Richard
---------------
I solved the problem in a different way.
When installing the I2S driver, there's an option to create an event queue, which flags TX_DONE (or RX_DONE).
I then created an xTask that sampled the queue, processing an I2S buffer each time the queue indicated one was completed.
To make sure I wasn't waiting for a buffer to clear, I measure the time it takes to write an I2S buffer. If it's longer than 50uS, then I've had to wait for I2S, so I skip the next TX_DONE flag.
It seems to work OK.
Hope this helps you.
Richard
---------------
Code: Select all
setup(){
static QueueHandle_t i2s_event_queue;
i2s_driver_install(i2s_num, &i2s_config, I2S_Q_LEN, &i2s_event_queue);
xret = xTaskCreate(I2Sout, "I2Sout", 20000, NULL, 1, &I2StaskHandle);
}
void I2Sout(void * params)
{
while(1)
{
TXdoneEvent = false;
qCnt = 0;
do // wait on I2S event queue until a TX_DONE is found
{
retv = xQueueReceive(i2s_event_queue, &i2s_evt, 1); // don't let this block for long, as we check for the queue stalling
if ((retv == pdPASS) && (i2s_evt.type == I2S_EVENT_TX_DONE)) //I2S DMA finish sent 1 buffer
{
TXdoneEvent = true;
qCnt++;
break;
}
vTaskDelay(1); // make sure there's time for some other processing if we need to wait!
} while (retv == pdPASS);
if(TXdoneEvent)
{
lastWrite = micros();
ESP_ERROR_CHECK(i2s_write(i2s_num, (const void*)&inBuf[thisBuf].data, LBUFSIZ*sizeof(uint16_t), &bytesOut, portMAX_DELAY ));
lastWrite = micros() - lastWrite; // used to determine when to drop the next packet - I2Swrite had to wait for a buffer to empty
}
}
}
-
- Posts: 3
- Joined: Fri Mar 05, 2021 12:54 am
Re: I2S - detect empty DMA queue
Thanks palmerr23! I didn't think a task could solve my problem. I managed to generate exactly a single wave form in these conditions:
On the I2S configuration, I created 4 buffers to match the exact data length:
This is the code to generate the pulse wave form:
When half the buffers are sent (don't know why), I ask to clear the DMA buffers. If I wait more or less TX_EVENTs, the pulse appears glitched. Here's the task handler function:
Maybe you can use this strategy to avoid counting time?
On the I2S configuration, I created 4 buffers to match the exact data length:
Code: Select all
#define NUM_SAMPLES 160
#define DMA_BUF_CNT 4
#define DMA_BUF_LEN (NUM_SAMPLES / DMA_BUF_CNT)
// Note that DMA_BUF_CNT x DMA_BUF_LEN = NUM_SAMPLES
uint32_t buf[NUM_SAMPLES];
volatile bool pulseOn = false;
volatile uint32_t txEvents = 0;
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
.sample_rate = 100000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = (i2s_comm_format_t)I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = 0,
.dma_buf_count = DMA_BUF_CNT,
.dma_buf_len = DMA_BUF_LEN,
.use_apll = 0
};
Code: Select all
void sendPulse() {
size_t bytesWritten;
i2s_write(I2S_NUM, buf, sizeof(buf), &bytesWritten, portMAX_DELAY);
txEvents = 0;
pulseOn = true;
digitalWrite(LED_BUILTIN, HIGH);
}
Code: Select all
void i2sTask(void*) {
while (true) {
i2s_event_t event;
// Wait indefinitely for a new message in the queue
if (xQueueReceive(i2s_queue, &event, portMAX_DELAY) == pdTRUE) {
if (pulseOn and event.type == I2S_EVENT_TX_DONE) {
if (++txEvents == DMA_BUF_CNT/2) {
// Stop the pulse
pulseOn = false;
i2s_zero_dma_buffer(I2S_NUM);
digitalWrite(LED_BUILTIN, LOW);
}
}
}
}
}
Who is online
Users browsing this forum: No registered users and 135 guests