SerialUSB, external interrupts and RingBuffer

Hello,

I'm working on a project where 56 speakers are suppose to play at the same time. The approach thus far is to generate the signals in a PC (MATLAB), send data to 7 Arduinos via SerialUSB and then to sync the sound by playing on external interrupts to the arduinos.

Because the RAM of the Arduinos are limited we need to send data and play at the same time, if we want to play audio in more than a couple of seconds. To do this we use RingBuffer: GitHub - Locoduino/RingBuffer: A RingBuffer library for Arduino
This makes having a buffer that is continiously filled and emtied much easier. And for the interrupts we use another arduino.

-------- PROBLEM --------
When we send a square wave to one arduino it plays with and frequency of ~ 200-300, altho the interrupt frequency is 8000hz (confirmed with ocilloscope).

Have we reached the limits of the Arduino? Is the problem that we have try using SerialUSB and external interrupts at the same time (if I'm correct Serial uses interrupts aswell)? Do we need pullup resistor to make the signal stronger? Can Arduino Due handle inputs of 8kHz to one of it's digital pins?

#include <RingBuf.h>

#define CONT_FLAG             0
#define STORE_FLAG           1
#define PLAY_FLAG              2
#define RECORD_FLAG       3
#define NUM_LM                    8
#define BUFFER_SIZE           8192
#define CHUNK_SIZE            8

#define maskA (1<<2)
#define maskB (1<<1)
#define maskC (1<<3)

#define port PIOC
#define a 34
#define b 33
#define c 35

const int interruptPin = 31;
int analogPins[] = {A2, A3, A4, A5, A6, A7, A8, A9};
int modePins[] = {22, 23, 24, 25, 26, 27, 28, 29};
int now = 0;
int rate = 120; // 120 = 8kHz (fs)

// Volatile don't work ?? 
RingBuf<byte, BUFFER_SIZE> ringBuf0;
RingBuf<byte, BUFFER_SIZE> ringBuf1;
RingBuf<byte, BUFFER_SIZE> ringBuf2;
RingBuf<byte, BUFFER_SIZE> ringBuf3;
RingBuf<byte, BUFFER_SIZE> ringBuf4;
RingBuf<byte, BUFFER_SIZE> ringBuf5;
RingBuf<byte, BUFFER_SIZE> ringBuf6;
RingBuf<byte, BUFFER_SIZE> ringBuf7;

void setup() {
  pinMode(DAC1, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP);
  pinMode(a, OUTPUT);
  pinMode(b, OUTPUT);
  pinMode(c, OUTPUT);
  pinMode(DAC1, OUTPUT);
  for (int i = 0; i < NUM_LM; i++) {
    pinMode(modePins[i], OUTPUT);
    pinMode(analogPins[i], INPUT_PULLUP);
  }

  // Setup the USB connection
  SerialUSB.begin(115200);
  delay(1000);
  attachInterrupt(digitalPinToInterrupt(interruptPin), playInterrupt, CHANGE);
  while (!SerialUSB) {}
}

void setSpeakerMode() {
  for (int i = 0; i < NUM_LM; i++) {
    // Set all L/M-units in speaker-mode
    digitalWrite(modePins[i], LOW);
  }
}


void mux(int val) {
  switch (val) {
    case 0:
      port->PIO_CODR = maskA | maskB | maskC;
      //Serial.println("000");
      break;

    case 1:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskC;
      //Serial.println("001");
      break;

    case 2:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskB;
      //Serial.println("010");
      break;

    case 3:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskC | maskB;
      //Serial.println("011");
      break;

    case 4:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskA;
      //Serial.println("100");
      break;

    case 5:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskC | maskA;
      //Serial.println("101");
      break;

    case 6:
      port->PIO_CODR = maskA | maskB | maskC;
      port->PIO_SODR = maskA | maskB;
      //Serial.println("110");
      break;

    case 7:
      port->PIO_SODR = maskA | maskB | maskC;
      //Serial.println("111");
      break;
    default:
      //Serial.println("Numbers only in range 0-7.");
      break;
  }
}

void contStore() {
  SerialUSB.flush();
  setSpeakerMode();
  boolean sending = true;
  byte data[CHUNK_SIZE] = {}; 
  
  while(sending)
  { 
    // Data aviable?
    if (SerialUSB.available() > 0) 
    { 
      // More data to send?
      if (SerialUSB.read() == 1)
      {
         // The queues are full?
         if (!ringBuf0.isFull()) 
         {
            // Buffer is not full, add one byte to them all.
            SerialUSB.write(byte(1));
            SerialUSB.readBytes(data, CHUNK_SIZE); // To be pushed to circular buffer
            ringBuf0.push(data[0]);
            ringBuf1.push(data[1]);
            ringBuf2.push(data[2]);
            ringBuf3.push(data[3]);
            ringBuf4.push(data[4]);
            ringBuf5.push(data[5]);
            ringBuf6.push(data[6]);
            ringBuf7.push(data[7]);
            // Confirm that all has been recived and buffered.
            // SerialUSB.write(byte(1));
          }
          else 
          {
            // Buffers were full, inform sender of this. 
            SerialUSB.write(byte(0));
            delay(1);
          }
       }
       else 
       {
          // The flag was 0 => no more data to receive
          sending = false;
       }
    }
    else
    {
      delay(1);
    }
  }
  // Session has ended
  SerialUSB.write(byte(3));
}

void playInterrupt() {
  byte data = 0;

  if (ringBuf0.pop(data))
  {
    mux(0);
    analogWrite(DAC1, data);
  }
  if (ringBuf1.pop(data))
  {
    mux(1);
    analogWrite(DAC1, data);
  }
  if (ringBuf2.pop(data))
  {
    mux(2);
    analogWrite(DAC1, data);
  }
  if (ringBuf3.pop(data))
  {
    mux(3);
    analogWrite(DAC1, data);
  }
  if (ringBuf4.pop(data))
  {
    mux(4);
    analogWrite(DAC1, data);
  }
  if (ringBuf5.pop(data))
  {
    mux(5);
    analogWrite(DAC1, data);
  }
  if (ringBuf6.pop(data))
  {
    mux(6);
    analogWrite(DAC1, data);
  }
  if (ringBuf7.pop(data))
  {
    mux(7);
    analogWrite(DAC1, data);
  }
}



void loop() {
  if (SerialUSB.available() > 0)
  {
    byte flags = SerialUSB.read();
    byte command = getCommand(flags);
    int numOfBytes = getNumOfBytes(flags);
    if (command == CONT_FLAG)
    {
      contStore();
    }
    else if (command == STORE_FLAG)
    {
      store(numOfBytes);
    }
    else if (command == PLAY_FLAG)
    {
      play(numOfBytes);
    }
    else if (command == RECORD_FLAG)
    {
      record(numOfBytes);
    }
  }
}

To Make this shorter I have removed a couple of functions not relevant to the issue. The functions of main concern is playInterrupt() and contStore(). We know that contStore() stores correctly, we can send long arrays and get them back via USB.

I have tried using the lockedPOP and lockedPush, and it didn't make any difference.

The speakers seem to play corectly, just much slower than 8000 samples per second.

Thanks for your time. :slight_smile:

Post schematics of your hardware!

Am I right that you're using an Arduino Due? Always tell us the model you use if it's not an UNO!

When we send a square wave to one arduino it plays with and frequency of ~ 200-300, altho the interrupt frequency is 8000hz (confirmed with ocilloscope).

How do you measure the 200-300Hz frequency? Do you use the mux pins for that?

To Make this shorter I have removed a couple of functions not relevant to the issue. The functions of main concern is playInterrupt() and contStore(). We know that contStore() stores correctly, we can send long arrays and get them back via USB.

Are you completely sure that you haven't removed a part that is essential? Does the setup show the same behavior with this shortened code? If the answer is no, post the complete code!

I am unsure to fully understand your code....

Some thoughts:

  • A DUE can handle interrupts from an attachinterrupt() at a much higher frequency than 8 KHz.

  • analogWrite() is relatively slow, DAC registers direct programing would be much better (see example sketches in the DUE sub forum)

  • How do you drive 56 speakers from 7 DUE thru DAC1 ?

I fear that you did not understand serial transmission and multiplexing of continuous analog signals. A circuit diagram of your electronic equipment were helpful. Please understand "circuit diagram", photos of circuits are of almost no help.

Serial input at 10k bytes/second and distribution to 7 S&H units will leave at best 1k samples for each channel. That's far away from 48k sps as used with sound cards.

I also doubt that ring buffers will improve anything, it were easier and faster to hand on every arrived serial byte to the right output channel.

pylon:
Am I right that you're using an Arduino Due? Always tell us the model you use if it's not an UNO!

Yes, sorry, we use DUE arduinos. Every Arduino has 8 speakers. The signal goes from the arduino to the speaker by using the pin DAC1 and an multiplexer. We do use UNO's for as clock generator tho.

pylon:
How do you measure the 200-300Hz frequency? Do you use the mux pins for that?

You can use a spectrum analyzer on your phone in front of the speaker. Or you can use an oscilloscope connected to the DAC1 pin. For this the code is changed somewhat, so we only play on one speaker from the DUE.

pylon:
Are you completely sure that you haven't removed a part that is essential? Does the setup show the same behavior with this shortened code? If the answer is no, post the complete code!

Yes. The other code is functions not used in this case. The post would be twice as long and serve no purpose.

ard_newbie:

  • A DUE can handle interrupts from an attachinterrupt() at a much higher frequency than 8 KHz.

That is nice.

ard_newbie:

  • analogWrite() is relatively slow, DAC registers direct programing would be much better (see example sketches in the DUE sub forum)

Will defiantly look into this.

ard_newbie:

  • How do you drive 56 speakers from 7 DUE thru DAC1 ?

By multiplexing. We have an multiplexer we use through the mux function. So every interrupt (8kHz), we set the mux and set DAC1 8 times. See interruptPlay().

DrDiettrich:
I fear that you did not understand serial transmission and multiplexing of continuous analog signals. A circuit diagram of your electronic equipment were helpful. Please understand "circuit diagram", photos of circuits are of almost no help.

We have schematics of the speakers. I don't know how this will help. The hardwere is basically an USB cable, an mux and a cable to the speaker.

DrDiettrich:
Serial input at 10k bytes/second and distribution to 7 S&H units will leave at best 1k samples for each channel. That's far away from 48k sps as used with sound cards.

You might be absolutely right. But i have heard somwhere that SerialUSB ingnores baudrate (unlike Serial) and are much faster.

DrDiettrich:
I also doubt that ring buffers will improve anything, it were easier and faster to hand on every arrived serial byte to the right output channel.

We thought a buffer could be an good idea, so we easier could get an constant play rate, and sync all 56 speakers. And that is much easier with an FIFO structure.

I need to clarify that synchronization of the speakers is great importance. We are building a beamforming MIMO-system. The interference, and thus the beamforming will not work if the 56 speakers are not synced (play their sample at the same time).

What you say is already being worked on by another member in the team. It seems quite promising, and is of course much easier to do.

Clarification:
Al tho we want 56 speakers and 7 DUE's in the end.

Now I'm working on one Due with one speaker connected. (but still load samples to all 8 buffers).

When i send square wave to the Arduino DUE i.e. 255, 0, 255, 0, 255, 0 ....
The speakers plays a note of frequency ~200 Hz, given a sampling freq of 8000 Hz, the played tone should be 8000/2 = 4000 Hz.

To me this means the Arduino only enters the interrupt 2*200 = 400 times/s
Or that the buffers don't load fast enough.

I wonder if it's even possible to do this. at an reasonable sampling frequency. Our speakers can only play up to ~3000 Hz, so we an sampling frequency of at least 6000 would be good.

All of this also need to be synced. This is of great importance.

Ok

Whenever you have to debug a software, begin small and progress baby step by baby step...

Try firstly to output correctly a square wave with a single DUE and a single speaker ( plus an amplifier in between ?) and no mux (and don't forget to connect at least 1.5K Ohms resistor in serie with DAC1 to avoid burning your DAC).

ard_newbie:
Ok

Whenever you have to debug a software, begin small and progress baby step by baby step...

We have done so.

  • We know we can send and receive data carrectly.
  • We know the buffers save correctly, and that pop and push works as expected.
  • We have played songs on the speakers (but since we a re limited to the PROGMEM or RAM, we can only play a couple of seconds this way.). The sound is fine.
  • All of this has also been tested with the mux in the mix.

This issue right now is that it works, just to slowly. When we try to play and load data at the same time. I wounder if it, given the Arduino DUE is an impossible task, geven it's performance.

There is something I don't understand: If you mux DAC1 to several speakers (e.g.2), what voltage receives speaker 1 while DAC1 is forwarded to speaker 2 (and vice versa) ?

ard_newbie:
There is something I don't understand: If you mux DAC1 to several speakers (e.g.2), what voltage receives speaker 1 while DAC1 is forwarded to speaker 2 (and vice versa) ?

There is some leakage. But it dose not seem that significant, that is the impression when just listening to the speakers. We have not measured the voltage. This could easily been done with an multi meter of course. I don't se the importance of this, care to elaborate?

What's the reference of your mux ?

I can imagine this solution for 1 DUE and 8 speakers:

You log the 8 new samples via the Native USB port. In the meantime, with DAC1 you output the 8 previous samples with a DMA so that it's done in parralel. There is no need for an external clock to trigger DAC output, it can be done by the DUE itself with a Timer Counter.

Note : if e.g. the input sin wave (for speaker1) frequency is F Hz (for the other speakers this will be the same frequency, whatever the wave), to restitute properly this sin wave at DAC1 you have to sample this sin wave at a much higher frequency, e.g. F64. This frequency (F64) is the "refresh" frequency of speaker1.

Because DAC1 has to "refresh" 8 speakers (speaker1, speaker2,....,speaker8), you have to receive samples at least at F648 Hz to fill a buffer (sample1, sample2,...,sample8). F648 Hz is also the frequency to trigger DAC1 output and mux.

I guess you are not using a SD card because the 8 input waves are not known beforhand ?

ard_newbie:
What's the reference of your mux ?

I don't know, I can look tomorrow.

ard_newbie:
I can imagine this solution for 1 DUE and 8 speakers:

You log the 8 new samples via the Native USB port. In the meantime, with DAC1 you output the 8 previous samples with a DMA so that it's done in parralel. There is no need for an external clock to trigger DAC output, it can be done by the DUE itself with a Timer Counter.

We are trying something like this, i don' t know about the DMA tho, will look into. The thing I see as possibly problematic is the syncronization.

  • Are the clocks of all the Arduinos synced if this is done?
  • If we load and play, doesn't the Arduinos just play one after the other then? we want them to play at the same time.

ard_newbie:
Note : if e.g. the input sin wave (for speaker1) frequency is F Hz (for the other speakers this will be the same frequency, whatever the wave), to restitute properly this sin wave at DAC1 you have to sample this sin wave at a much higher frequency, e.g. F64. This frequency (F64) is the "refresh" frequency of speaker1.

Because DAC1 has to "refresh" 8 speakers (speaker1, speaker2,....,speaker8), you have to receive samples at least at F648 Hz to fill a buffer (sample1, sample2,...,sample8). F648 Hz is also the frequency to trigger DAC1 output and mux.

Don't know if i follow. The number of samples each arduino needs to recievice is 188000 = 64000 bytes/s ? And for a buffer to be useful the received rate much be higher than this. This is the essence of my question in a way. Are the rates possible.

ard_newbie:
I guess you are not using a SD card because the 8 input waves are not known beforhand ?

Yes kind of. We could work more with predetermined signals, or have long loading periods between every session. But we would prefer to be able to play from all speaker from the MATLAB script directly. We need this because we channel estimate using a pilot and then generate audio to the speakers from this.