2

tl;dr: The sample rate of my code seems to be lower than what I expect it to be. How can I fix this?

Director's cut: I am trying to sample audio data and send it via serial. The setup consists of a guitar plugged into an amplifier (about 10x) + low-pass filter (second order, around 1kHz) + voltage divider (centered at 2.5V), that is then connected to and Arduino Uno Rev3. When the Arduino receives a 'cap' command via serial, it enables the interrupt-based function that takes samples and sends them via Serial. With the 10-bit resolution, it gives me a number between 1 and 4 digits. With a baud rate of 115200 and adding 1 byte for the '\n' character, the slowest rate is of 2880 values per second, but most likely around 3.6k values per second (3+1 characters).

From what I understood (not used to working with timer interrupts), I've set up timer1 so that it doesn't have any prescaling, and it should be running pretty close to 2048Hz. The interrupt sequence reads the data at the analog input and sends it via Serial (for now, to the computer; later, a smartphone via Bluetooth).

To check if it is working, I am copying the data received by the computer, putting it into Matlab and running the sound(data, sampleRate) function. If I run it with 2048 as sampleRate, I get a clearly higher pitched sound, which indicates that the sample rate is in fact lower. Testing different values, I found that it sounds close enough around 1900.

From what I've read, the serial and the timer interrupt work in parallel, and the serial is faster than the capture. Why am I getting this apparently lower sample rate? How can I fix this?

Code:

// Variables
const int sampleRate = 2048; // Sampling rate
const int numSamples = 2048; // Number of Samples
//volatile int samples[numSamples]; // Data array
int currentSample = 0;
boolean capturing = false; // flags if it is in capture mode
boolean done = false; // flags if the capture is done
char chrCommand; // holds current character read
String strCommand; // joins the complete command string

// Functions
void captureSample();
void readCommand();
void executeCommand();

void setup() {
  /****************
   * Timer Setup
   ****************/
  cli(); // stop interrupts
  // set timer1 interrupt at 2048Hz
  TCCR1A = 0; // set the entire TCCR1A register to 0
  TCCR1B = 0; // set the entire TCCR1B register to 0
  TCNT1  = 0; // initialize counter value to 0
  // set compare match register for 2048Hz increments
  OCR1A = 7811; // = (16*10^6) / (sampleRate) - 1
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // no prescaling
  TCCR1B |= (1 << CS10);
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  sei(); // allow interrupts

  /****************
   * Serial Setup
   ****************/
  Serial.begin(115200);


  pinMode(13,OUTPUT);
}

void loop(){
  if(!capturing){
    // Case: waiting for command
    if(!done){
      if(Serial.available() > 0){
        readCommand();
        executeCommand();
      }
    }else{
      done = false;
    }
  }
}

/************************
 * Function Definitions
 ************************/

// Timer function
ISR(TIMER1_COMPA_vect){
  if(capturing){
    captureSample();
  }
}

void captureSample(){
  //samples[currentSample] = analogRead(0);
  Serial.println(analogRead(0));
  ++currentSample %= numSamples;
  if(currentSample == 0)
  {
    done = true;
    capturing = false;
  }
}

void readCommand(){
  strCommand = "";
  while(Serial.available() > 0)
  {
    chrCommand = ((byte)Serial.read());
    if(chrCommand == '\n')
    {
      break;
    } else
    {
      strCommand += chrCommand;
    }
    delay(1);
  }
}

void executeCommand(){
  // Command cap = capture
  if(strCommand == "cap"){
    capturing = true;
    return;
  }
  // Command mov = move
  if(strCommand == "mov"){
    // get the motor movement in the following command
    readCommand();
    moveMotor();
    return;
  }
  if(strCommand == "clr"){
    done = false;
    capturing = false;
    for(int i = 0; i < 2; ++i){
      digitalWrite(13,HIGH);
      delay(50);
      digitalWrite(13,LOW);
      delay(50);
    }
    return;
  }
}
  • 1
    If you run with different (higher) baud rates does the sample frequency change? – Majenko Jul 8 '16 at 17:54
  • I've increased the baud rate and it seems to be working fine. For now, I think this will work, unless something limits my baud rate. Thank you! – bhashimoto Jul 8 '16 at 18:52
5

You calculation of the data rate is wrong:

  • Serial.println() adds CRLF (i.e. "\r\n") to terminate the line, That's 2 bytes, not one.
  • Each byte is sent by the serial port as one start bit, followed by 8 data bits, followed by one stop bit. That's 10 bits in terms of timing.

In the worst case scenario (value ≥ 1000), you are sending 6 bytes, or 60 bits: the fastest possible rate is 1920 samples per second. In the arguably most common case (100 ≤ value < 1000), you send 50 bits per sample at up to 2304 S/s.

Edit: Here are a few optimization tips, just to make sure your program is not CPU-bound:

  • Declare captureSample() as static, this way the compiler can inline it and save the call overhead.
  • Make currentSample unsigned, this way the compiler can optimize the modulo operation into a bitwise AND.
  • Replace analogRead(0) with a direct read of the ADC register, and start the next ADC conversion right away. This way you do not have to wait for 104 µs while the ADC does its work.
  • You may want to move the Serial.println() out of the ISR, otherwise you may be blocking interrupts for too long if the serial port cannot cope with the sampling rate.
  • Make some kind of binary protocol instead of using plain ASCII.

You may also trigger the ADC directly from a timer instead of going through the timer ISR (see the datasheet for details). You would then use ISR(ADC_vect) to retrieve the ADC result. This way you get a very steady (hardware controlled) sampling rate.

More about the binary protocol: you could send only two bytes per sample and remain ASCII-compatible (which is often desirable) by using 32 printable characters to represent 5 bits. This is the same idea used in base64, but simplified to use consecutive characters:

uint8_t high_5_bits = sample >> 5;
uint8_t low_5_bits  = sample & 0x1f;
Serial.write('@' + high_5_bits);
Serial.write('@' + low_5_bits);

This gives a stream of characters in the range @A-Z[\]^_. Then your maximum sampling rate is 5760 S/s.

  • Following @Majenko's suggestion, I've increased the baud rate and it seems to work fine. Still, I will try to implement these tips, make it better. Thank you very much for the answer! – bhashimoto Jul 8 '16 at 18:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.