Close

Performance Concerns

A project log for Street Sense

Portable electronic device to measure air and noise pollution

mike-teachmanMike Teachman 12/15/2018 at 06:000 Comments

My previous log discussed the Asynchronous programming approach being used in this project.

As a quick reminder, the key concept of Asynchronous programming is co-operative scheduling of coroutines that need processing time.  Each coroutine is "trusted" to only run for a minimal amount of time, then give control back to the scheduler, so that other coroutines can run... very cooperative and nice.

The ambition for noise analysis is to record a gapless stream of audio samples to an external SD Card that can be later post-processed and characterized.  The high level functions in the audio processing loop are shown below.

This flowchart depicts a continuous loop that is 100% dedicated to audio sample processing.  But, the sensor unit has other tasks that need to run.  For example, there are tasks that need to read the ozone, NO2, and PM2.5 particulates sensors.  There is a display task to update the OLED display.  And,  eventually there will be a MQTT task pushing sensor readings to the cloud.  If the audio loop ran continuously, no other task could ever be serviced.   

With asynchronous programming,  a coroutine that runs in a loop must periodically give control back to the scheduler so that other tasks can run - this is called a yield.   Control is yielded to the uasyncio scheduler using a call to await asyncio.sleep(0).  Three yield points in the microphone handler are shown below.

Problems !

The 3 yield calls were added to the loop and an audio recording was made using a 20 kHz sampling rate.  The audio playback showed "choppy audio", indicating gaps in the recording.  What is happening?

First, some background on constraints in the processing of audio samples

1)  Every loop, 256 audio samples are read from the microphone.  If the sampling rate is 20kHz, the sampling period is 256/20kHz = 12.8 ms.  On average, the microphone loop needs to complete every 12.8ms.  Otherwise, samples will be missed.  

2) Audio samples are first buffered into a chain of DMA memory blocks.  There are limitations in DMA buffering.  A total of 16 kBytes of DMA memory is used to buffer the incoming audio samples.  That amount of buffering can hold 102.4 ms of samples.  This means that the microphone loop can be blocked from running a total of 102.4 ms in the worst case.  If it is blocked for longer, then the DMA buffer will overrun and samples will be lost.

I added some print() statements into the loop to better understand the time of each operation.  The unexpected surprise was the await asyncio.sleep(0) call.  This call gives control back to the scheduler, giving other tasks the opportunity to run (if they are ready).  I expected that the call to await asyncio.sleep(0) would return very quickly (e.g. < 1ms) when no other tasks are queued to run.  This is not the case.  It took a typical 6 ms to return control back to the audio processing loop even when no other tasks were queued to run.  What's the big deal? - 6 ms is a blink in time.  But, it represents a rather high percentage of time for the overall audio sample processing loop time (12.8ms), especially if 3 calls to asyncio.sleep(0) are made in each loop.

Only having one call to asyncio.sleep(0) eliminated the gaps in the recording.  Still, this amount of "wasted" time in the scheduler is concerning.  When the microphone task yields to the scheduler more than one task may run.  If every task switch takes 6ms I have doubts that gapless audio recording is feasible with uasyncio.

At this point, I'm having thoughts like "I should do this project in C/C++" which I believe would eliminate these inefficiencies.  But, I'm still fairly committed to a MicroPython implementation - it's just so liberating to use this rich language and skip the time-sucking compiling phase.

The hope

I posted my design issues to the MicroPython forum and received some good feedback.  I learned that there is an alternative implementation of the MicroPython uasyncio library called fast_io.   Fast_io has a modified scheduler that can give priority to critical tasks, like the audio sample processing task.

The next step is to evaluate the fast_io library.  More soon ...

Discussions