Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying I2S buffer sizes and counts #3790

Closed
wants to merge 3 commits into from

Conversation

earlephilhower
Copy link
Collaborator

I've been doing a lot of audio work but am finding that sometimes the ESP8266 internal WiFi code can go off and do something for quite a long while (relative to audio delays, that is) causing the internal DMA buffers to empty and hiccups in the output to occur.

The simplest way to avoid this is to increase the size and number of RAM buffers we can pre-fill to cover these WiFi stack times. Granted, with only ~40K available you're not going to add seconds worth of buffering, but every little bit can help.

Here's a backwards compatible patch to allow an app to specify a larger buffer size/count if necessary, while preserving the original behavior (and saving a few bytes in everyone else's RAM).

---snip---
Add a new i2s_begin_custom() signature that takes a buffer count and buffer
size (in samples). This allows for specifying a larger or smaller amount
of DMA data which can help cover periods when the ESP's internal WiFi
processing takes longer than the few milliseconds of data currently
allocated (i.e at 44.1KHZ, the original buffers would last for 512 samples
which works out to only 11.6 milliseconds).

This also frees up ~76 bytes of RAM in all applications that do not use
I2S since it dynamically allocates pointer/counter arrays and only has a
single static pointer defined.

Add a new i2s_begin_custom() signature that takes a buffer count and buffer
size (in samples).  This allows for specifying a larger or smaller amount
of DMA data which can help cover periods when the ESP's internal WiFi
processing takes longer than the few milliseconds of data currently
allocated (i.e at 44.1KHZ, the original buffers would last for 512 samples
which works out to only 11.6 milliseconds).

This also frees up ~76 bytes of RAM in all applications that do not use
I2S since it dynamically allocates pointer/counter arrays and only has a
single static pointer defined.
i2s_slc_queue_len = 0;
int x, y;

i2s_slc_queue = (uint32_t*)malloc((SLC_BUF_CNT-1) * sizeof(uint32_t));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that this malloc is 1 item smaller than the others? I realize this is how it is currently implemented, but I have to wonder.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the malloc is done, the pointer should be checked against null, and free()d if not.

Copy link
Collaborator Author

@earlephilhower earlephilhower Jan 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIze is -1 by design (see below, the original code only alloc'd buf_cnt-1). Good catch on 2nd comment. It's safe to free(NULL), so I'll always free before allocing.

However, in the case they are not free'd already that means we potentially have DMA running and the ISR hooked up. There's a race condition then where the DMA could touch the free'd buffer or the ISR could work with these arrays, only to find we'd NULL'd them. Bad stuff.

I'll do an i2s_end() which completely resets the i2s subsystem, clearing everything and stopping any dma, right at the beginning. In this case there will be no frees in begin(), as they're guaranteed free'd in the i2s_end().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably don't want to free and malloc again if the call is done with the same size arg.

i2s_slc_queue_len = 0;
int x, y;

i2s_slc_queue = (uint32_t*)malloc((SLC_BUF_CNT-1) * sizeof(uint32_t));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the malloc is done, the pointer should be checked against null, and free()d if not.

@@ -50,10 +53,10 @@ struct slc_queue_item {
uint32 next_link_ptr;
};

static uint32_t i2s_slc_queue[SLC_BUF_CNT-1];
static uint32_t *i2s_slc_queue;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ptrs should be inited to null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are, actually. In ANSI-C, static variables are set to all 0 and live in BSS, even if you don't explicitly list it. I'm happy to add in the assignment, of course, to make it absolutely clear, of course.

@@ -148,8 +157,14 @@ void ICACHE_FLASH_ATTR i2s_slc_end(){
SLCRXL &= ~(SLCRXLAM << SLCRXLA); // clear RX descriptor address

for (int x = 0; x<SLC_BUF_CNT; x++) {
free(i2s_slc_buf_pntr[x]);
free(i2s_slc_buf_pntr[x]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ptrs here should be nulled after free.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, good point, will do!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, scratch that. i2s_slc_buf_pntr[] is a dynamically allocated array of pointers. I free each pointer i2s_slc_buf_pntr[0...n], then completely free and null i2s_slc_buf_pntr. So there is no way to get to those old pointers, ever, so no need to NULL.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am guarding against is a second call to begin() without a call to end(), I.e. make sure no leaks. This seems to be common usage in ArduinoLand.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arduinoland is a dangerous place to live...

It'll still (randomly, making it even more fun) crash w/o the changes I mentioned on the i2s_begin() call due to ISR and DMA working w/the old addresses.

If the user calls i2s_begin() while DMA is already running, the ISR
or DMA engine could fire and use the old addresses after they're
freed or even while we're working on them.  Avoid this by forcing a
i2s_stop() before doing any work.

These changes effectively allow is2_begin() and i2s_end() to be called
any number of times, in any order, without crashing.
@earlephilhower
Copy link
Collaborator Author

I'm going to punt on this as it looks like I2S may be undergoing a larger change related to supporting input. It's small enough to regenerate in the future if there is interest.

@earlephilhower earlephilhower deleted the i2cparams branch November 18, 2020 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants