Achieving a Sampling Frequency of 8kHz

Hello,

I'm in a project, wherein the goal is to get the RMS value of the AC voltage from the power outlet (60Hz).

The code I have so far can be seen below.

int pinV=0;
int pinVref=4;
int i=0;
float sumV_rms;
int Ts=1;
float V[2];
float base;
int counter;
float valueV_rms;

float readVolt(int pinV){ //Transforms the value read by analogRead into volts.
    return (5*analogRead(pinV))/1023.0;
}

void setup() {
 Serial.begin(115200);

}

void loop() {
  if(i==0){ //Getting the first values of V.
    V[i]=readVolt(pinV);
    i=1;
   }
   else{ //Run continuosly after the first run.
      V[1]=V[0];    // V[1] receives the old value.
      
      V[0]=readVolt(pinV);  //V[0] receives a new value
      
      base=readVolt(pinVref)/2; // the offset is given by this relationship
      
      if(V[1]< base && V[0]>base){ // When it crosses the base value:
 
          V[1]=V[0]; // V[1] receives the old value.
          V[0]=readVolt(pinV); // V[0] received a new value
          counter=1; // This is going to count the number of samples in the period
                   
          sumV_rms=0; 
          while(!((V[1]< base)&&(V[0]>base))){ //Until the next time the voltage crosses the 'base value' from down up
              
              sumV_rms=sumV_rms+pow(V[0],2); //it sums the squared values

              delayMicroseconds(125);
              
              V[1]=V[0];  // V[1] receives the old value
              V[0]=readVolt(pinV);  //V[0] receives a new value.
              
              counter++; //incrementing the counter

              
    
          }
          valueV_rms=sqrt(sumV_rms/(float)counter); //RMS calculation   
          Serial.println("Number of samples: " + String(counter)+", RMS value: " + String(valueV_rms));
 // Prints out the results
      }
    }
  }

The code is working fine, but I wanted to have some kind of control over my sampling rate.

I have this 'delayMicroseconds(125)' at the end of the while loop to make sure it is at 8kHz, but from the number of samples, I can tell it's not at 8kHz.

If I take 8,000 samples/second and my wave has a period of 1/60 seconds, then I should have 8,000/60=133,33 samples in one period, and I'm getting 55~57 samples / period using this code.

Is there a way to make sure my sampling frequency is at 8kHz?

Thank you in advance!

You should use micros() for timing, not delayMicroseconds().

aarg:
You should use micros() for timing, not delayMicroseconds().

do you mean that if((micros()-previousTime)>=125){} thing?

aarg:
You should use micros() for timing, not delayMicroseconds().

From what I understood, I was able to make the following adustments.

 while(!((V[1]< base)&&(V[0]>base))){ //Until the next time the voltage crosses the 'base value' from down up
              
              sumV_rms=sumV_rms+pow(V[0],2); //it sums the squared values

              int to=micros();
             
              if((micros()-to)>=125){
              
              V[1]=V[0];  // V[1] receives the old value
              V[0]=readVolt(pinV);  //V[0] receives a new value.
              
              counter++; //incrementing the counter
              }
              
    
          }

Now I'm getting around 93 samples/period, which is amazing! But is there a way to get something closer to 133? Because it has to be really precise.

Thank you :slight_smile:

micros() times are unsigned long, not int. Your timing logic doesn't look right. It should roughly follow the Blink without Delay approach. "to" should be initialized inside the test, not before it. The idea is, you're making a time stamp.

unsigned long to;
[...]
 while([... you need to just count in here ...]){ //Until the next time the voltage crosses the 'base value' from down up
              if((micros()-to)>=125){
              to=micros();

              V[0]=readVolt(pinV);  //V[0] receives a new value.
              sumV_rms=sumV_rms+pow(V[0],2); //it sums the squared values                                      
              counter++; //incrementing the counter
              }              
          }

I broke out and removed the zero crossing detection. You should separate that, it has no business inside the measurement loop. You do know what the frequency is, right?

Why don't you set up your own timer interrupt routine, with a basic clock time of 0.125 ms. I believe with a 16 MHz clock, you can get that exactly as a divider (of course, the 16 MHz isn't ever perfect). Then use the timer interrupt to take a reading. You then are no longer at the mercy of the various arduino time functions, which to the best of my knowledge are only roughly accurate. I set up a time-of-day clock just using a 1 KHz timer, which stays within 1 second accuracy for days, so the timer must be pretty darn fgood.

This info might be of help to you.
Use jrdoner's idea of setting up a timer to sample at required frequency. I suggest the Timer1 (or Timer3) library here to make setting one up a lot easier.

The pow(x, 2) function is a lot slower than x*x, for floats. You should fix that, right off the bat.

If you want to speed things up even more, you can use scaled fixed point integer arithmetic during the sampling time, and convert to float when you're finished.

      while (!((V[1] < base) && (V[0] > base))) { //Until the next time the voltage crosses the 'base value' from down up
        sumV_rms = sumV_rms + pow(V[0], 2); //it sums the squared values
        delayMicroseconds(125);
        V[1] = V[0]; // V[1] receives the old value
        V[0] = readVolt(pinV); //V[0] receives a new value.
        counter++; //incrementing the counter
      }

To expand a bit on what @aarg said.
Each time that you call readvolt you are doing a multiplication followed by a floating point division. The division is very slow.
You can factor out a lot of the computation and leave it until the code exits this loop.
As an example let's assume the loop accumulates four ADC samples S1, S2, S3 and S4 - each a 10-bit integer sample from AnalogRead.
When they are sampled, readVolt changes each one into a floating point number S1*(5/1023.), S2*(5/1023.), S3*(5/1023.), S4*(5/1023.).
As each one is accumulated, it is squared so that after the fourth sample you have:

sumV_rms = S1*(5/1023.)*S1*(5/1023.) + S2*(5/1023.)*S2*(5/1023.) + S3*(5/1023.)*S3*(5/1023.) + S4*(5/1023.)*S4*(5/1023.);

Each term on the right-hand side is multiplied by a constant factor of (5/1023.)*(5/1023.) which can be factored out to give:

sumV_rms = (5/1023.)*(5/1023.) * (S1*S1 + S2*S2 + S3*S3 + S4*S4);

Because of this, in the loop itself you only need to accumulate the sum of squares of the samples themselves (i.e. the part in the parentheses) so that in the loop all that you calculate is this, (where sumV_rms can now be an unsigned long instead of a float):

sumV_rms = S1*S1 + S2*S2 + S3*S3 + S4*S4;

The sampling and accumulation within the loop can now be done entirely with integer arithmetic which is much faster than what you have. Then outside the loop you finish off the calculation like this:

     valueV_rms = 5/1023.*sqrt(sumV_rms / (float)counter); //RMS calculation

Note that multiplying by 5/1023. outside the sqrt is equivalent to multiplying by (5/1023.)*(5/1023.) inside the sqrt.
And the (float) cast on 'counter' will convert the division to floating point so that you don't have a problem with integer division.

One last thing. It should be 1024. throughout - not 1023.

Pete
P.S. Don'tcha love algebra? :slight_smile:

How much time are you losing due to serial printouts? This tripped me up on a different project until I tracked it down. I see you've used Serial.begin(115200); rather than Serial.begin(9600); but it still costs a bit of time.

I got around it by collecting data and calculating for a second then I stopped collecting and calculating to do a printout then started over.

Just a thought.