8000 esp32,stm32/i2s: I2S protocol support for Pyboards and ESP32. by miketeachman · Pull Request #7183 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

esp32,stm32/i2s: I2S protocol support for Pyboards and ESP32. #7183

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

Closed
wants to merge 5 commits into from
Closed

Conversation

miketeachman
Copy link
Contributor

This PR adds I2S protocol support for STM32 and ESP32 ports:

  • consistent I2S API across the STM32 and ESP32 ports
  • I2S configurations supported:
    • master transmit and master receive
    • 16-bit and 32-bit sample sizes
    • mono and stereo formats
    • specify sampling frequency
    • 3 modes of operation:
      • blocking
      • non-blocking with callback
      • uasyncio
    • specify internal ring buffer size
  • MicroPython documentation for Pyboards and ESP32
  • new uasyncio Stream class method "readinto()"
  • tested on the following development boards:
    • Pyboard D SF2W
    • Pyboard V1.1
    • ESP32 with SPIRAM
    • ESP32
  • builds on the STM32 work of blmorris:
  • I2S examples repo to support this PR:
  • this PR obsoletes an ESP32-specific I2S PR:
  • pybv1.0 metric changes wrt master: text(+5592), data(0), bss(+8)
  • esp32 metric changes wrt master: text(+11228), data(+2832), bss(+24)

"buf" byte ordering is little-endian. For Stereo format, left channel sample precedes right channel sample.
Returns number of bytes written

.. method:: I2S.callback(handler)
Copy link
Member

Choose a reason for hiding this comment

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

To match other classes (eg Pin, BLE), I'd suggest renaming this method to I2S.irq(handler).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for flagging that. I tried to keep consistency with other modules, but I missed that one. I'll update the PR.

Copy link
Contributor Author
@miketeachman miketeachman May 2, 2021

Choose a reason for hiding this comment

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

I added a new PR commit that brings consistency to the callback naming.

@dpgeorge
Copy link
Member
dpgeorge commented May 1, 2021

Fantastic work, thank you! I especially like the "3 modes of operation" part. I'll need to find some time (and I2S hardware) to test this.

self,
I2S_TASK_PRIORITY,
(TaskHandle_t *)&self->non_blocking_mode_task,
MP_TASK_COREID) != pdPASS) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if there is a possibility to allow this task to run on the PRO esp32 core?

Unlike _thread which pins new threads to the APP core there is no sharing of Micropython data and all inter task communication goes through the FreeRTOS queue.

I read some more above and my description is not quite accurate. The mp_buffer_into_t passes in from the micropython script and the non_blocking_descriptor_t contains a pointer to it. When reads or writes happen in the non blocking task they touch the buffer data.

But if the callbacks are used then it reads like it should be safe because they are invoked after processing on that buffer is done.

I'll see what happens if the core is changed and report back.

Maybe its too niche and I should just patch this in my firmware but if it works it might be a useful switch to use where you have too much processing to do and also sample gap-less audio on one esp32 core.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm interested to see what happens as well. Thanks for volunteering to try this out.

Copy link
Contributor

Choose a reason for hiding this comment

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

It took me a few days to upgrade my firmware to use this new version.

I'm trying to run the tensorflow micro_speech example and because of the machine learning model, working area, etc I don't have enough RAM to allocate 40k + 10k to buffers like your record to SD card example.

It looks like its fast enough when the task is running on the APP core for my use-case.

I'm using 320 x 5 x 4 (6400) bytes for the i2s dma buffer and 320 x 2 (640) bytes read into buffer.

The x 4 on the dma buffer is because your comments say it samples 32 bit integers and then throws out non-applicable byes to scale the i2s dma samples into the read into buffer.

But I also changed the task to pin to core 1 (PRO core) and it seems to run faster.

I need to do more analysis to quantify the speedup.

When running I'm having trouble getting it to stop consistently using control-c.

If the code running is micropython then it can get caught by the except block and stop.

caught exception KeyboardInterrupt 
Done
Traceback (most recent call last):
  File "main.py", line 396, in <module>
  File "main.py", line 361, in runModel
  File "main.py", line 86, in input_callback
  File "micro_speech.py", line 80, in setInputTensorValues
KeyboardInterrupt: 
MicroPython v1.15-66-g7aa713ace-dirty on 2021-05-04; ESP32 module with ESP32
Type "help()" for more information.
>>> 

But about half the time I see this instead and the result is a soft reboot which starts up the program again.

caught exception KeyboardInterrupt 
Done
/opt/esp/idf/components/freertos/queue.c:764 (xQueueGenericSend)- assert failed!

abort() was called at PC 0x40093d4c on core 0

Backtrace:0x40092ca2:0x3ffce5e0 0x400933e9:0x3ffce600 0x40098db2:0x3ffce620 0x40093d4c:0x3ffce690 0x40132789:0x3ffce6d0 0x4020876b:0x3ffce710 0x4010868d:0x3ffce740 0x40101655:0x3ffce770 0x40107321:0x3ffce7a0 0x4010744e:0x3ffce7c0 0x401098a9:0x3ffce7e0 0x401016d4:0x3ffce880 0x40107321:0x3ffce8b0 0x40107409:0x3ffce8d0 0x4012fd04:0x3ffce900 0x4012fdf0:0x3ffce980 0x4012fe55:0x3ffce9a0 0x4013175f:0x3ffce9c0 0x40131a39:0x3ffcea50 0x40131a9d:0x3ffcea70 0x4011a0f0:0x3ffcea90


ELF file SHA256: 4d5c617ffc8e19ad

Rebooting...
ets Jun  8 2016 00:22:57

rst:0xc (SW_CPU_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:5516
ho 0 tail 12 room 4
load:0x40078000,len:13800
ho 0 tail 12 room 4
load:0x40080400,len:3388
entry 0x40080634
Fatal exception (29): StoreProhibited
epc1=0x40079fd8, epc2=0x00000000, epc3=0x00000000, excvaddr=0xa2f40000, depc=0x00000000
ets Jun  8 2016 00:22:57

rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:5516
ho 0 tail 12 room 4
load:0x40078000,len:13800
ho 0 tail 12 room 4
load:0x40080400,len:3388
entry 0x40080634

Copy link
Contributor Author
@miketeachman miketeachman May 6, 2021

Choose a reason for hiding this comment

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

It took me a few days to upgrade my firmware to use this new version.

Awesome ! Which version of ESP-IDF did you use?

your record to SD card example

Which example are you using? e.g. blocking, non-blocking, uasyncio

The x 4 on the dma buffer is because your comments say it samples 32 bit integers and then throws out non-applicable byes to scale the i2s dma samples into the read into buffer.

There is no need to be concerned with buffer ratios in the MicroPython code. For example, you could define an internal buffer of 10k and a readbuffer of 1k, or an internal buffer of 8k and a readbuffer of 20k. The C code implementation shields that complexity from the user. It was accidental that the C code comment uses 10k and 40k (which is the same as the MicroPython examples). I will change the 10k and 40k in the C code comment to eliminate any confusion on this point.

Copy link
Contributor

Choose a reason for hiding this comment

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

I setup on your i2s branch and built using the latest 4.3 idf. I'm building using the espressif docker image: espressif/idf:release-v4.3.

I modified the non-blocking example:
https://github.com/miketeachman/micropython-i2s-examples/blob/master/examples/record-mic-to-sdcard-non-blocking.py

Thanks for working on this pull request for so long. I think it will open up a lot of opportunities for driving LED's and various other projects that previously could only be done using C or C++.

}
} else { // Master Rx
// read stereo frames for all I2S hardware
return I2S_CHANNEL_FMT_RIGHT_LEFT;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is great work.

Can you explain why you always read both left and right channels? It's quite unusual for people to have two microphones connected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You bring up a good point. I need to take a second look at this implementation. Valuable DMA buffer space is wasted for 16-bit and 32-bit Mono modes. Thank you for reviewing this code and making suggestions. I appreciate it !

Copy link
Contributor

Choose a reason for hiding this comment

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

No problem - I'm really glad I found the PR and samples.

// in the I2S constructor. e.g. 16-bit mono
STATIC const int8_t i2s_frame_map[NUM_I2S_USER_FORMATS][I2S_RX_FRAME_SIZE_IN_BYTES] = {
{ 6, 7, -1, -1, -1, -1, -1, -1 }, // Mono, 16-bits
{ 4, 5, 6, 7, -1, -1, -1, -1 }, // Mono, 32-bits
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this assumes that Mono always means left - this is probably true in most cases as most microphones will default to left. But it might be worth calling this out in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is right - Mono Rx mode only uses the Left channel audio data. Great idea to mention this in the MicroPython I2s documentation - I will make this improvement.

// the left and right channels
// https://github.com/espressif/esp-idf/issues/6625
REG_SET_BIT(I2S_CONF_REG(self->port), I2S_TX_MSB_RIGHT);
REG_SET_BIT(I2S_CONF_REG(self->port), I2S_RX_MSB_RIGHT);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still required - I think it might have been fixed on esspressif's side of things.

Copy link
Contributor Author
@miketeachman miketeachman May 13, 2021

Choose a reason for hiding this comment

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

Yes - the workaround is still needed. This bug still exists in the versions of ESP-IDF used by MicroPython. I have not seen any indication that Espressif has fixed the bug in new versions of the ESP-IDF.

@cgreening
Copy link
Contributor

I'm not sure if it would belong in this PR or as an extension to the work, but some higher-level abstractions would be really helpful for people - particularly people just starting out.

Being able to stream audio directly to and from WAV files (either to flash or SD Cards) would be really nice.

e.g. something along these lines, where you could kick off recording or playback in a non-blocking way. For the playback, adding volume adjustments would be a really nice additional feature.

start_recording("filename.wav")
....
stop_recording()
start_playing("filename.wav")
...
stop_playing()

@wgaylord
Copy link

Would being able to use the internal ADC and DAC for the esp32 version be hard to add? It could be useful for some people if they don't want an external I2S device.

@pumelo
Copy link
pumelo commented May 28, 2021

I'm already using your old i2s esp32 (#4471) implementation with success with an ics43432 mic. Thank you very much @miketeachman 💐
I'll upgrade to this PR soon ;-)

One thing which begs me: currently there is no way to detect an overflow when reading right?

@miketeachman
Copy link
Contributor Author

some higher-level abstractions would be really helpful for people - particularly people just starting out.

This is a really good idea. For example, I can imagine creating a new class Wavplay using MicroPython that implements a WAV file play function. All of the low level details to play a WAV file would be contained in the Wavplayclass.

It might work as follows

from machine import I2S
import Wavplay

i2s = I2S(... I2S config parameters...)
wav_file = open("/sd/{}".format("audio.wav"), "rb")

wav_player = Wavplay(i2s, wav_file)

wav_player.play()
wav_player.pause()
wav_player.resume()
wav_player.stop()
wav_player.volume(level)

@miketeachman
Copy link
Contributor Author

Would being able to use the internal ADC and DAC for the esp32 version be hard to add?

I think it could be added to MicroPython, but it might be best implemented in the machine.DAC class. This approach was taken in the Lobo ESP32 port. https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/dac#dacstopwave

The machine.I2S class is solely focused on interfacing to hardware that implements the I2S protocol.

@miketeachman
Copy link
Contributor Author

currently there is no way to detect an overflow when reading right?

Sorry, but I am having some difficulty to understand the question. Is it possible to provide an example? thanks!

@dpgeorge
Copy link
Member

new uasyncio Stream class method "readinto()"

This change will need to be put into a separate commit. I'm happy to do that if needed.

@wgaylord
Copy link

Would being able to use the internal ADC and DAC for the esp32 version be hard to add?

I think it could be added to MicroPython, but it might be best implemented in the machine.DAC class. This approach was taken in the Lobo ESP32 port. https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/dac#dacstopwave

The machine.I2S class is solely focused on interfacing to hardware that implements the I2S protocol.

I am mostly interested in the ADC side of it where I can sample audio using the internal ADC using the I2S system, because the normal way of using the ADC on esp32 can't get very high sample rates, let alone once you stuff Micropython between you and the ADC.

And having it in the correct machine class for the ADC would make sense but that means when using the ADC connected over I2S you would have the machine class for the ADC touching the I2S driver stuff that this class uses possibly messing up state info.

@cgreening
Copy link
Contributor

Would being able to use the internal ADC and DAC for the esp32 version be hard to add?

I think it could be added to MicroPython, but it might be best implemented in the machine.DAC class. This approach was taken in the Lobo ESP32 port. https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/dac#dacstopwave
The machine.I2S class is solely focused on interfacing to hardware that implements the I2S protocol.

I am mostly interested in the ADC side of it where I can sample audio using the internal ADC using the I2S system, because the normal way of using the ADC on esp32 can't get very high sample rates, let alone once you stuff Micropython between you and the ADC.

And having it in the correct machine class for the ADC would make sense but that means when using the ADC connected over I2S you would have the machine class for the ADC touching the I2S driver stuff that this class uses possibly messing up state info.

This is a very good point. Both the internal ADCs and internal DAC can be used via I2S - quite a few people use this for recording and playing back audio.

@miketeachman
Copy link
Contributor Author

This change will need to be put into a separate commit.
Sure. I could pull out the readinto() extension into a separate commit, then submit a PR for this commit. Would that be an acceptable approach?
@dpgeorge

@dpgeorge
Copy link
Member

Both the internal ADCs and internal DAC can be used via I2S

Do you mean that the ADC can be used as an input source to the I2S peripheral, and the DAC and output source? So it's:

  • CPU <- I2S <- ADC <- microphone
  • CPU -> I2S -> DAC -> speaker
    ?

@cgreening
Copy link
Contributor
cgreening commented May 29, 2021 via email

@dpgeorge
Copy link
Member

I could pull out the readinto() extension into a separate commit, then submit a PR for this commit. Would that be an acceptable approach?

Yes please! We'd need to add docs and a test for it as well.

It's not in CPython but it sounds like they will eventually add it: https://bugs.python.org/issue41305

@dpgeorge
Copy link
Member

That’s correct. I have some sample code for both cases.

Then that should be a relatively simple enhancement to the API here. Maybe something like i2s.connect(adc) or adc.connect(i2s).

@cgreening
Copy link
Contributor
cgreening commented May 29, 2021

There's a bit of funkiness on the processing the samples from the ADC inputs - sample code here should show how to do it - https://github.com/atomic14/esp32_sdcard_audio

@miketeachman
Copy link
Contributor Author

I am mostly interested in the ADC side of it where I can sample audio using the internal ADC using the I2S system, because the normal way of using the ADC on esp32 can't get very high sample rates, let alone once you stuff Micropython between you and the ADC.

And having it in the correct machine class for the ADC would make sense

I can imagine a new machine.ADC class method such as machine.ADC.readinto(buf) for reading audio sample data from an analog microphone. Referring again to the LoBo port, something similar has been implemented using the I2S peripheral:
https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/adc#data-collection

but that means when using the ADC connected over I2S you would have the machine class for the ADC touching the I2S driver stuff that this class uses possibly messing up state info.

Good point. This situation would need careful consideration if the two I2S peripherals on the ESP32 become shared between machine module classes, such as machine.I2S, machine.ADC, or machine.DAC. In the LoBo port, only one of the ADC and DAC classes is allowed to use an I2S peripheral.

@wgaylord
Copy link

I am mostly interested in the ADC side of it where I can sample audio using the internal ADC using the I2S system, because the normal way of using the ADC on esp32 can't get very high sample rates, let alone once you stuff Micropython between you and the ADC.
And having it in the correct machine class for the ADC would make sense

I can imagine a new machine.ADC class method such as machine.ADC.readinto(buf) for reading audio sample data from an analog microphone. Referring again to the LoBo port, something similar has been implemented using the I2S peripheral:
https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo/wiki/adc#data-collection

but that means when using the ADC connected over I2S you would have the machine class for the ADC touching the I2S driver stuff that this class uses possibly messing up state info.

Good point. This situation would need careful consideration if the two I2S peripherals on the ESP32 become shared between machine module classes, such as machine.I2S, machine.ADC, or machine.DAC. In the LoBo port, only one of the ADC and DAC classes is allowed to use an I2S peripheral.

This is very true because also you can have the issue the other way, say the I2S class adds using the ADC or DAC as input and output, you have to worry about the ADC or DAC class trying to use that same peripheral without knowing that its in use by the I2S peripheral. I feel like some sort of shared state would be needed to tell each what device is truly in use with I2S.

@miketeachman
Copy link
Contributor Author
miketeachman commented May 29, 2021

Then that should be a relatively simple enhancement to the API here. Maybe something like i2s.connect(adc) or adc.connect(i2s)

For me, the preferred approach is to extend the machine.ADC and machine.DAC classes to support analog audio devices. This approach matches hardware capabilities to the intended machine class abstractions. Another way to look at this: The ESP32 I2S peripheral is multi-purpose - it can be used to support cameras, I2S audio hardware, LCD screens, and analog audio hardware. This does not mean that all these features should be supported by the MicroPython machine.I2S class.

@dpgeorge
Copy link
Member

For me, the preferred approach is to extend the machine.ADC and machine.DAC classes to support analog audio devices.

You mean like stm32 has dac.write_timed() and adc.read_timed()?

Whatever the way it's done, that should not affect this I2S API which seems pretty solid.

New name is shorter a
EED3
nd allows for the future option of
supplying internal buffer memory in the constructor.
@miketeachman
Copy link
Contributor Author
miketeachman commented Jun 29, 2021

If you can rebase on that, that would be great.

I rebased against the master. I didn't squash the 3 PR commits. I could do that step as well, but at this point I thought it might be better to see the individual commits.

As far as I can tell, these words/terms do not appear in any of the user-facing API functions/classes/methods/constants

I didn't see anything in the API either that should be an issue. It's only in the C code and commit comments.

};

// This is used by the DMA callback functions to access the I2S object
STATIC machine_i2s_obj_t *machine_i2s_obj[MAX_I2S_STM32];
Copy link
Member

Choose a reason for hiding this comment

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

This will probably need to go in the root pointer section, eg like pyb_uart_obj_all. Then access it via MP_STATE_PORT(machine_i2s_obj).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I held off on this change as I don't understand the issue or risk. I had considered adding this array to the root pointer section, but the array is holding references to I2S objects, which are allocated in the recommended way, and should not be prematurely GC'd. I can see a different situation for "stranded" heap objects that are allocated using m_new() and held inside the C code, and not connected to any structure that the GC can see - in that case I understand the need to declare a root pointer, so the "stranded" object is not GC'd.

Copy link
Member

Choose a reason for hiding this comment

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

You are right that if the users code holds a reference to the object then they will not be GC'd. The issue comes about if the user creates an object, registers an IRQ callback, then no longer holds on to it. Then it can get GC'd and the IRQ will run with invalid data. This is pretty easy to do with Pin and Timer IRQ callbacks. It's probably also possible to do with an I2S object, but if you think it's ok as-is then let's leave it, and possibly change it later if the need arises.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I hadn't considered the possibility of a race condition. I can see that possibility in the I2S implementation. I'll definitely make the root pointer fix that you recommend, in both the STM32 and ESP32 ports. There is nothing more heinous than a non-reproducible crash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I started work on the root pointer fix, but it seems there are some tricky aspects to adding root pointers to esp32/mpconfigport.h.

For example, in the root pointer section I need to add a line for I2S - shown below. To compile this I need a way to declare the macro I2S_NUM_MAX . Unfortunately adding the relevant header file #include driver/i2s.h causes many compile errors. Substituting I2S_NUM_MAX with the magic number 2 gets everything compiling, but that's not a very maintainable solution.

Rather than continue reverse engineering the code, I thought it best to ask for help ...

#define MICROPY_PORT_ROOT_POINTERS \
    const char *readline_hist[8]; \
    mp_obj_t machine_pin_irq_handler[40]; \
    struct _machine_timer_obj_t *machine_timer_obj_head; \
    struct _machine_i2s_obj_t *machine_i2s_obj[I2S_NUM_MAX]; \
    MICROPY_PORT_ROOT_POINTER_BLUETOOTH_NIMBLE

Copy link
Member

Choose a reason for hiding this comment

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

Ok, interesting... some options:

  1. make machine_i2s_obj a void* and in i2s_init0() allocate (on the GC heap) an array of the right size and type and point the root pointer to it
  2. #define MP_I2S_MAX 2 in mpconfigport.h and static assert that it's less than or equal to I2S_NUM_MAX somewhere in machine_i2s.c

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Using a compile-time assert is a good fit for this problem. I'll move forward with option 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made a commit adding machine_i2s_obj[] to the root pointer section. Builds and testing on my local machine worked, but two builds on the server failed after commit. I took quick look and one is easy to fix (nucleo build), but the other build (ESP32/IDF) shows a non-obvious failure. Unfortunately, I don't have any time before holidays to fix these issues. I decided to back out the commit.

I can fix these build problems when I return from holidays in 2 weeks and a bit. The root pointer concern doesn't seem like a showstopper. Maybe the PR can be merged and I'll commit a new PR on the root pointer fix when I get back? Or, just wait...

Copy link
Member

Choose a reason for hiding this comment

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

Let's merge. We can fix it later.

@dpgeorge
Copy link
Member

I did not test this on hardware but am confident it works 😄

The only outstanding discussion point would be about the io mode: blocking vs non-blocking vs uasyncio. Here we have I2S automatically configuring itself based on:

  • blocking: default, or if i2s.irq(None) is called (ie IRQ disabled)
  • non-blocking: selected via i2s.irq(<callback>) (ie IRQ enabled)
  • uasyncio/polling: selected if i2s.ioctl(...) is called

For comparison:

  • The bluetooth module always operates in non-blocking mode with IRQ callbacks, and then uasyncio.ThreadSafeFlag can be used to hook that into uasyncio (or any other async-based framework).
  • Socket/TCP streamsare either blocking or non-blocking, and non-blocking mode works automatically with ioctl (polling) and uasyncio. These streams are configured via socket.setblocking(...).

My feeling is that it would be nice if I2S fit in either of the existing models for blocking/non-blocking... would it be possible to make non-blocking and uasyncio IO modes the same, and then add i2s.setblocking() to explicitly configure the stream behaviour?

changes resulting from code review comments:
- make object allocation consistent with other peripherals by
  reusing the existing I2S object when any other I2S objects are
  instantiated using the same I2S peripheral
- misc:  change "busses" to "buses"
         fix function scope errors (global->file scope)
@miketeachman
Copy link
Contributor Author

Your summary of the operational modes is bang on. My goal with this I2S module is to have an API that is easy to master and "just works". Also working identically across the ports was paramount. The latter goal took more effort than I'd like to admit. I'd like to keep these two goals when changes to the operation of the module are considered.

My feeling is that it would be nice if I2S fit in either of the existing models for blocking/non-blocking..

Thanks for sharing these examples of existing models. I'll take a look at the two examples and see if there is a fit for this I2S PR. One thing ... I'm on summer vacation starting in a few days so any meaningful re-work won't start for about 3 weeks.

@dpgeorge
Copy link
Member

My goal with this I2S module is to have an API that is easy to master and "just works".

That's a good goal, and matches how most of the other machine classes are (although none are as complex as I2S in terms of blocking/non-blocking streams). For comparison, the bluetooth module has a higher-level wrapper called aioble https://github.com/micropython/micropython-lib/tree/master/micropython/bluetooth/aioble which gives more of a "just works" feeling.

Also working identically across the ports was paramount

Yes that is definitely the number 1 goal.

One thing ... I'm on summer vacation starting in a few days so any meaningful re-work won't start for about 3 weeks.

Ok.

I know how much work has already been put into this PR (thank you!), and it has also been getting some real-world use. So I'd be happy to merge it as-is -- so it doesn't linger too long here -- but merged with an understanding that there could be some tweaks to the API in the future. Is that reasonable?

@miketeachman
Copy link
Contributor Author
miketeachman commented Jun 30, 2021

I know how much work has already been put into this PR (thank you!), and it has also been getting some real-world use. So I'd be happy to merge it as-is -- so it doesn't linger too long here -- but merged with an understanding that there could be some tweaks to the API in the future. Is that reasonable?

I think the time is right to get this module into wider use, get feedback, fix bugs, then tune the implementation and API based on user feedback and observed patterns of use. What if the PR was merged as-is (with the addition of the root pointer fix that I'll make later today), but the I2S docs flag the module as "preliminary" or "technology preview"? This approach would advertise that the API might change in the future. We could leave the module in a preview form for a couple of releases.

Even though I've put a lot of time into the module I'm definitely not resistant to future changes. For example, a desirable outcome might be reducing the volume of C code, adding python wrappers, while maintaining functionality, performance, and usability. I imagine there are a lot more developers who are skilled in python rather than embedded C. Less C code would seem like a win. MicroPython has enabled a lot of my embedded projects and I teach a MicroPython course at our local Victoria Makerspace. I see future support of I2S as one way of giving back to the MicroPython community.

@dpgeorge
Copy link
Member

What if the PR was merged as-is (with the addition of the root pointer fix that I'll make later today), but the I2S docs flag the module as "preliminary" or "technology preview"?

Yes, let's do it!

MicroPython has enabled a lot of my embedded projects and I teach a MicroPython course at our local Victoria Makerspace. I see future support of I2S as one way of giving back to the MicroPython community.

Fantastic! Thanks for the feedback, always great to hear about how it's being used.

@miketeachman
Copy link
Contributor Author

What if the PR was merged as-is (with the addition of the root pointer fix that I'll make later today), but the I2S docs flag the module as "preliminary" or "technology preview"?

Yes, let's do it!

I submitted a commit that identifies the I2S class as a Technology Preview

@dpgeorge
Copy link
Member
dpgeorge commented Jul 5, 2021

Squashed and merged in 8a5bfe4!

Thank you @miketeachman for the huge effort to get this over the line.

@peterhinch
Copy link
Contributor

The docs are confusing this old codger.

ws_pin = Pin("Y5")    # Word clock output
sdout_pin = Pin("Y8") # Serial data output

audio_out = I2S(2,
                sck=sck_pin, ws=ws_pin, sdin=sdin_pin,
                mode=I2S.TX,
                bits=16,
                format=I2S.MONO,
                rate=44100,
                ibuf=20000)

audio_in = I2S(2,
               sck=sck_pin, ws=ws_pin, sdin=sdin_pin,
               mode=I2S.RX,
               bits=32,
               format=I2S.STEREO,
               rate=22050,
               ibuf=20000)

Should sdin=sdin_pin read sd=sdin_pin? This would conform with the list of constructor args.

Secondly the setups call for sdin_pin whereas only sdout_pin is declared and sdin_pin seems unexpected for an output.

As a more general point, I'm using the hardware designed by @blmorris who did the initial work on I2S. This worked within the limits of his software.

I started out by reading a WAV file off the Pyboard's SD card. I'm pretty sure I'm reading the data segment OK, but the playback is noise not music. I'm using the blocking writer but I suspect that the rate I can read data from the disk may be inadequate. Do you have any general comments on how best to play substantial files on I2S/Pyboard?

In the meantime I'll try much smaller samples and input.

@peterhinch
Copy link
Contributor

Another query, this time on the nonblocking docs which read:

audio_in.irq(i2s_callback) # i2s_callback is called when buf is filled
num_read = audio_in.readinto(buf) # returns immediately

Presumably num_read can't convey much information if it returns immediately.

More importantly, the callback appears to offer the possibility of the user implementing a double buffering scheme where the callback issues .readinto after switching buffers and flags the user code to process the data just read. This poses the question of whether this can happen fast enough to avoid data loss. Should the callback be issued before the buffer is completely full? There would then need to be some way of knowing how much data it held at the instant of buffer switching - I can't help thinking there is still potential for data loss. Or should buffer switching be done in the C code?

In practice I intend to use the uasyncio interface but I am rather puzzled over how (or if) nonblocking mode can be used to acquire longer sequences.

@peterhinch
Copy link
10000
Contributor

I'm making no headway here. I've written a script which creates a single cycle of a sinewave comprising 44 samples and saves it to a file. The following script attempts to play it back 10,000 times at 44Ksps using the blocking driver. It blocks for 10s as expected, but the result is silence. Obviously I can't expect you to debug the hardware setup but it did make some noise in my earlier tests.

I'd be grateful if you could point out anything daft that I'm doing. Hardware is a Pyboard 1.1 running today's daily build. The relevant part of the script is:

sck_pin = Pin("Y6")   # Serial clock output
ws_pin = Pin("Y5")    # Word clock output
sdout_pin = Pin("Y8") # Serial data output

audio_out = I2S(2,
                sck=sck_pin, ws=ws_pin, sd=sdout_pin,
                mode=I2S.TX,
                bits=16,
                format=I2S.STEREO,
                rate=44100,
                ibuf=20000)

hwsetup()  # This sets up @blmorris hardware for stream output
with open('sin', 'rb') as f:
    buf = f.read()
for x in range(0, len(buf), 4):
    l, r = struct.unpack('<hh', buf[x: x+4])
    print(l, r)

for cycles in range(10000):
    audio_out.write(buf)

and the printout of left and right channels is

0 0
1423 1423
2817 2817
4154 4154
5406 5406
6548 6548
7557 7557
8412 8412
9096 9096
9594 9594
9898 9898
10000 10000
9898 9898
9594 9594
9096 9096
8412 8412
7557 7557
6548 6548
5406 5406
4154 4154
2817 2817
1423 1423
0 0
-1423 -1423
-2817 -2817
-4154 -4154
-5406 -5406
-6548 -6548
-7557 -7557
-8412 -8412
-9096 -9096
-9594 -9594
-9898 -9898
-10000 -10000
-9898 -9898
-9594 -9594
-9096 -9096
-8412 -8412
-7557 -7557
-6548 -6548
-5406 -5406
-4154 -4154
-2817 -2817
-1423 -1423

@dpgeorge
Copy link
Member
dpgeorge commented Aug 5, 2021

Should sdin=sdin_pin read sd=sdin_pin? This would conform with the list of constructor args.

Secondly the setups call for sdin_pin whereas only sdout_pin is declared and sdin_pin seems unexpected for an output.

Yes I think you're right. I've raised #7617 to fix this.

But otherwise I don't have any hardware at the moment to test this.

@peterhinch
Copy link
Contributor
peterhinch commented Aug 5, 2021

Thanks for confirming. Hopefully @miketeachman will be able to comment on my code. It would be good if there was a recommended "official" hardware audio board and a couple of demo scripts for testing input and output. Then we'd have a good starting point for developing applications.

Incidentally the I2S functionality is present in daily builds, but if I build my own firmware it is absent from machine.

@mocleiri
Copy link
Contributor
mocleiri commented Aug 6, 2021

@peterhinch Are you aware of this repo with the comprehensive examples on how to use this pull request? https://github.com/miketeachman/micropython-i2s-examples

It says 3 boards were tested:

  1. INMP441 microphone module.
  2. MSM261S4030H0 microphone module.
  3. Adafruit I2S MEMS Microphone Breakout - SPH0645LM4H.

I don't think the blmorris board is one of these. That page also has the recommended pins to use per board but yours look to be correct (the ones from the sine example).

I'm setup and working on esp32 with an INMP441 mic in mono mode.

For the esp32 implementation I got no data(silence/static) when the mic was setup in right channel mode as mono was expecting the mic to be in left channel mode.

I don't have experience with the transmit code but on the receive code there is a translation process between the hardware sampling and the bytes put into the user_buffer which match the bit size and channels configured in machine.I2S.

One suggestion I have would be to try different mono/stereo and bit rate combinations to see if anything works. Or pick up one of the read wav from sd card examples and see if that will transmit properly.

I used a variant of this to validate my hardware setup: https://github.com/miketeachman/micropython-i2s-examples/blob/master/examples/record-mic-to-sdcard-uasyncio.py

There was a prior version that would read from the mic and save to the esp32 flash filesystem. I was able to record at 8000 and 16000 hertz.

I would record the sampled data to a wav file. Download and listen in windows media player to verify.

For the non-blocking mode you need to be sure to trigger the next i2s.readinto(user_buffer) within the callback.

https://github.com/miketeachman/micropython-i2s-examples/blob/d5a17c55c6550248e00a63701f06d6ef7b5b890d/examples/record-mic-to-sdcard-non-blocking.py#L92

When I first tried to implement I only called from the main program body and only read one buffer's full of data instead of getting a stream of data.

I think the i2s.readinto(user_buf) is a no-op in the listener but causes the registration of the user_buf to receive the data eventually (when available in the dma buffer).

In my testing the irq callback is invoked when the user_buf has been filled (or partially filled) and the num_read global will have the total bytes just read.

@peterhinch
Copy link
Contributor

Thank you very much. I'll order some suitable hardware.

@cgreening
Copy link
Contributor

I've got a very simple test program you can try that you can flash using Arduino if that helps - at least to confirm your hardware and pins are correct - https://github.com/atomic14/esp32-i2s-mic-test

@mocleiri
Copy link
Contributor
mocleiri commented Aug 7, 2021

I found the details for the @blmorris audio board: https://github.com/blmorris/uPy_AudioCodec
Its using an Analog Devices SSM2604 which is configured via i2c and then accessed using i2s.

I wonder if there is some special handling within @blmorris's i2s driver that was removed from the final code in this pull request.

current : https://github.com/micropython/micropython/blob/master/ports/stm32/machine_i2s.c

original:
https://github.com/blmorris/micropython/blob/i2s/stmhal/i2s.c

There do seem to be some special syncing logic but for when running in SLAVE mode; like this one: https://github.com/blmorris/micropython/blob/cd5556909883e8083a770c22f72b7908a430d95d/stmhal/i2s.c#L597

@peterhinch
Copy link
Contributor

Thanks all.

I've ordered a SPH0645LM4H and a MAX98357A from Adafruit as @miketeachman has tested with these. With his demos working I'll test with uasyncio which is my real interest. I worked with the @blmorris hardware as the only kit available - I may have a go at getting it working once I'm happy with the code.

@peterhinch
Copy link
Contributor
peterhinch commented Aug 11, 2021

[EDITED]
This forum thread may be of interest, also this issue.

tl;dr
I2S devices are very sensitive to the quality of electrical connection. Leads must be kept short. The WS signal drives an on chip phase locked loop to synchronise the chip's clocks. Long leads can cause problems ranging from dire sound quality to total failure. I suspect (but can't prove) that timing uncertainties on WS are the cause of this unusual level of sensitivity.

dpgeorge pushed a commit that referenced this pull request Nov 13, 2021
This change eliminates the risk of the IRQ callback accessing invalid data.
Discussed here:
#7183 (comment)

Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
IhorNehrutsa pushed a commit to IhorNehrutsa/micropython that referenced this pull request Nov 20, 2021
This change eliminates the risk of the IRQ callback accessing invalid data.
Discussed here:
micropython#7183 (comment)

Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
leifbirger pushed a commit to leifbirger/micropython that referenced this pull request Jun 14, 2023
This change eliminates the risk of the IRQ callback accessing invalid data.
Discussed here:
micropython#7183 (comment)

Signed-off-by: Mike Teachman <mike.teachman@gmail.com>
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.

7 participants
0