Search code examples
arduinoembedded

How do I print microphone input to the serial port of arduino nano 33 ble sense?


Good day,

I have an arduino nano 33 ble sense with a microphone on it. Using the PDM.h library I extract the audio into a sample buffer 512 bytes at a time. I use a type of ping pong buffer to save data while I print to the serial port but it keeps crashing, what did I do wrong?

#include <PDM.h>
#include <stdlib.h>

// buffer to read samples into, each sample is 16-bits
char sampleBuffer[512];

//ping pong buffer
//total bytes is how many bytes the microphone recorded
//write offset switches between first or second half of the ping pong buffer
//read offset switches between first or second half of the ping pong buffer
//ready to print indicates that the data is ready to print
char audio[66000];
long int totalBytes = 0;
int writeOffset = 0;
int readOffset = 0;
int half = 32768;
bool readyToPrint = false;


void setup() {

  //the sample rate is 16Khz with a 16-bit depth that means 32KBytes/s are needed to fully transfer this signal
  //the baud rate represents the bit rate of the signal, 32*8 is 256Kbits/s, closest compatible baud rate of the nano is 500kbaud
  Serial.begin(500000);
  while (!Serial);

  // configure the data receive callback
  PDM.onReceive(onPDMdata);

  
  // optionally set the gain, defaults to 20
  // PDM.setGain(30);

  // initialize PDM with:
  // - one channel (mono mode)
  // - a 16 kHz sample rate
  if (!PDM.begin(1, 16000)) {
    Serial.println("Failed to start PDM!");
    while (1);
  }
}

void loop() {
}

void printToSerial(){
  Serial.write(audio + readOffset, half);
  readOffset = readOffset ^ half;
}

void onPDMdata() {
  // query the number of bytes available
  int bytesAvailable = PDM.available();

  // read into the sample buffer
  PDM.read(sampleBuffer, bytesAvailable);
  

  if(totalBytes < half){
    memcpy(audio + writeOffset + totalBytes, sampleBuffer, bytesAvailable);
    totalBytes += bytesAvailable;
  }
  else{
    totalBytes = 0;
    writeOffset = writeOffset ^ half;
    printToSerial();
  }

}

Solution

  • The PDM library documentation like most Arduino documentation is pitifully low on critical technical detail, but you should probably treat the PDM receive data callback as if it were executing in the interrupt context.

    That would put a number of constraints on it, possibly including not outputting serial data, and certainly not blocking which attempting to output 32K of data in one go will almost certainly do unless the serial output buffer is exceptionally large. In fact there is a Sketch in the Nano 33 BLE examples called PDMSerialPlotter that confirms this in a comment:

    /**
     * Callback function to process the data from the PDM microphone.
     * NOTE: This callback is executed as part of an ISR.
     * Therefore using `Serial` to print messages inside this function isn't supported.
     * */
    void onPDMdata() {
    ...
    

    It does not appear that you are reading 512 bytes at a time as you suggest. That is simply your buffer size. In onPDMdata() you read the number of bytes available (without incidentally checking that is not larger than 512 bytes and causing an overrun of the buffer - although 512 is the default if you don't call setBufferSize(), but it is unwise to rely on that not changing).

    Apart from being poor practice to block a callback of any kind, blocking the PDM input processing for longer than the time taken to fill its buffer will clearly not work. At 16ksps, and a 256 (512 byte) sample buffer, your real-time deadline for dealing with the the PDM input is 160ms, whilst it will take 655ms to output 32768 bytes at 500kbaud. Unless the serial buffer can accommodate all 32768 bytes in one go, it will block waiting for the output to complete.

    Your so-called ping-pong buffer is nothing of the sort. In a ping-pong one half of the buffer is filled whist the other is emptied - in a separate context. You only have one context and are done with the half buffer you filled before you start filling the other.

    It would make more sense to output the PDM data as soon as it is acquired. There is no purpose in buffering it when the output rate is faster then the input rate:

    #include <PDM.h>
    #include <stdlib.h>
    
    // buffer to read samples into, each sample is 16-bits
    volatile char sampleBuffer[512];
    volatile int output_bytes = 0 ;
    static const int PDM_SAMPLE_BUF_LEN = sizeof(sampleBuffer) ;
    
    void setup() 
    {
      Serial.begin(500000);
      while (!Serial);
    
      // configure the data receive callback
      PDM.onReceive(onPDMdata);
      PDM.setBufferSize( PDM_SAMPLE_BUF_LEN ) ;
    
      if (!PDM.begin(1, 16000)) 
      {
        Serial.println("Failed to start PDM!");
        while (1);
      }
    }
    
    void loop() 
    {
        if( output_bytes > 0 )
        {
            Serial.write( sampleBuffer, output_bytes ) ;
            output_bytes = 0 ;
        }
    }
    
    void onPDMdata() 
    {
        if( output_bytes == 0 )
        {
            // query the number of bytes available
            int bytesAvailable = PDM.available();
    
            // read into the sample buffer
            output_bytes = PDM.read(sampleBuffer, bytesAvailable);
        }
    }
    

    Note that in the setup(), to guard against any possibility of a buffer overrun, you should explicitly set the buffer length regardless of any knowledge of a default.

    PDM.setBufferSize( sizeof(sampleBuffer)) ;
    

    All that said, I would a favour an implementation using a ring buffer, where you load data to the ring buffer in onPDMdata(), and read from the ring-buffer in loop() That then allows for any variable latency such as might be caused by flow control, although on average the serial output still needs ot be as fast ot faster then the PDB data rate. Something like:

    #include <PDM.h>
    #include <stdlib.h>
    #include <stdint.h>
    
    const int PDM_SAMPLE_BUF_LEN = 512 ;
    class cByteBuffer
    {
        public:
            cByteBuffer() : put_index(0),
                            get_index(0),
                            available(0),
                            buffer()
            {}
            
            int put( const uint8_t* data, int length )
            {
                int p = 0 ;
                for( p = 0; 
                     available <= sizeof(buffer) && p < length; 
                     p++ )
                {
                    buffer[put_index] = data[p] ;
                    available++ ;
                    put_index++ ;
                    if( put_index >= sizeof(buffer) )
                    {
                      put_index = 0 ;
                    }
                } 
                
                return p ;
            }
        
            int get( uint8_t* data, int max_length )
            {
                int g = 0 ;
                for( g = 0; 
                     available > 0 && g < max_length;
                     g++ )
                {
                    data[g] = buffer[get_index] ;
                    available-- ;
                    get_index++ ;
                    if( get_index >= sizeof(buffer) )
                    {
                      get_index = 0 ;
                    }
                }
                
                return g ;
            }
    
        private:
            int put_index ;
            int get_index ;
            int available ;
            unsigned char buffer[4096] ;
    } ;
    
    
    cByteBuffer audio_buffer ;
    
    void onPDMdata() 
    {
        // buffer to read samples into, each sample is 16-bits
        static uint8_t sampleBuffer[PDM_SAMPLE_BUF_LEN];
    
        // query the number of bytes available
        int bytesAvailable = PDM.available();
    
        // read into the sample buffer
        PDM.read(sampleBuffer, bytesAvailable);
    
        // Put data in ringbuffer
        audio_buffer.put( sampleBuffer, bytesAvailable ) ;
    }
    
    
    void setup() 
    {  
        Serial.begin(500000);
        while (!Serial);
    
        // configure the data receive callback
        PDM.onReceive(onPDMdata);
        PDM.setBufferSize( PDM_SAMPLE_BUF_LEN ) ;
    
        if (!PDM.begin(1, 16000)) 
        {
            Serial.println("Failed to start PDM!");
            while (1);
        }
    }
    
    void loop() 
    {
        static uint8_t output_buffer[32] ;
        int output_len = 0 ;
        
        while( 0 != (output_len = audio_buffer.get( output_buffer, sizeof(output_buffer) ) ) )
        {
            Serial.write( output_buffer, output_len ) ;
        }
    }
    

    Noting that I have not tested this - I do not have the hardware - it does however compile in Arduino Create.

    Your buffer audio (if t worked) would have be capable of holding more than 2 seconds of audio data (with the associated output delay). That sees excessive, above I have a ring buffer of 4096, or 120ms of audio data, compared with a maximum of 16ms in the PDM buffer. That will deal with moderate latency in the serial communication.

    A further consideration is the capability of the device you are sending this data to. It will have to be capable of receiving data at that rate. Also at that rate the nature of the physical connection may be critical for adequate signal integrity. If using a serial-USB bridge, some such devices may have insufficient buffering to sustain that rate - USB has significant latency and relies on the response and timeliness of the host polling for available data.