Go Down

Topic: Measuring amplifier output power (watts) (Read 9556 times) previous topic - next topic

Jiggy-Ninja

@ Magician: I'm running an Uno, so ATmega328p. I also have a Leonardo, but I doubt I'll be able to push that any faster since it's clocked the same.

I'm aware I won't be able to get full 20 - 20,000 readings. That's why my compromise is at 18,000 - 19,000 readings per second, which gives just a slight oversampling of the Nyquist rate for my 8,000 Hz low pass filter. 8,000 Hz is an ear-piercing shriek, and I don't think most music goes that high anyway. The sacrifice I've chosen for this algorithm is to drop some samples while calculating the RMS voltage of the buffer, as well as some of the higher frequencies which might not even be significant to begin with.

This speaker won't be running for months or years at a time, so I think finding some way to calculate the DC offset at startup is probably best.

@dlloyd: If I make the RC filter that low, won't it just average everything out to the DC offset? That would be useless to measure.
Hackaday: https://hackaday.io/MarkRD
Advanced C++ Techniques: https://forum.arduino.cc/index.php?topic=493075.0

Magician

Last year I did a project, audio VU meter. Same arduino UNO, sampling rate 40 000 , calculates RMS and dB.
Code: [Select]
#include <glcd.h>                     // http://playground.arduino.cc/Code/GLCDks0108
#include <avr/pgmspace.h>

#include "fonts/allFonts.h"           // system and arial14 fonts are used
#include "bitmaps/allBitmaps.h"       // all images in the bitmap dir

#define  SMP_RATE                   40       // Sampling Rate, in kHz
#define  SMP_TMR1 ((16000/SMP_RATE) -1)      // Sampling Period of Timer1

/* VU Meter / The audio level meter most frequently encountered is the VU meter. Its characteristics are
defined as the ANSI specification C165. Some of the most important specifications for an AC meter
are its dynamic characteristics. These define how the meter responds to transients and how fast the reading
decays. The VU meter is a relatively slow full-wave averaging type, specified to reach 99% deflection in
300 ms and overshoot by 1 to 1.5%. In engineering terms this means a slightly underdamped second order
response with a resonant frequency of 2.1 Hz and a Q of 0.62.
While several European organizations have specifications for peak program meters, the German DIN specification
45406 is becoming a de facto standard. Rather than respond instantaneously to peak, however, PPM specifications re-
quire a finite "integration time" so that only peaks wide enough to be audible are displayed. DIN 45406
calls for a response of 1 dB down from steady-state for a 10 ms tone burst and 4 dB down for a 3 ms tone burst.
These requirements are consistent with the other frequently encountered spec of 2 dB down for a 5 ms burst and
are met by an attack time constant of 1.7 ms. The specified return time of 1.5s to ?20 dB requires a 650 ms
decay time constant.*/

            Image_t      icon;
            gText        countdownArea =  gText(GLCD.CenterX, GLCD.CenterY,1,1,Arial_14);
           
             int16_t     adc_Offst =   512;
volatile     int32_t     ppm_Level =     0;
             float       rms_Level =   0.0;
//             int16_t     x10_coeff =    10;
           
ISR(TIMER1_COMPB_vect)
{
  int32_t temp = ADC - adc_Offst;

          temp = temp * temp;
         
  if ( temp > ppm_Level ) ppm_Level = ((ppm_Level * 255) + temp) >> 8;
  else ppm_Level = (ppm_Level * 16383) >> 14;   
}

void   Draw_Table()
{
  GLCD.CursorToXY( 3,  3);
  GLCD.Puts("-20 10 5 3 1 0 1 2 3");
  GLCD.CursorToXY( 5, 52);
  GLCD.Puts("VU meter");
  GLCD.CursorToXY(75, 52);
  GLCD.Puts("Magician");
  GLCD.DrawRoundRect( 0, 0, 126, 63, 5); 
  GLCD.DrawLine( 64, 62, 5, 10, BLACK ) ;
  GLCD.DrawLine( 63, 62, 4, 10, BLACK ) ;
}

void   Draw_Arrow( int32_t scale )
{
  static int st1 = 5;
  static int st2 = 5;

  st2 = map( scale, 20, 300, 5, 122);     //  23.5 dB 

  if ( st2 > 122 ) st2 = 122;
  if ( st2 <   5 ) st2 =   5;

  if ( abs(st1 - st2) > 3 )               // 1/3 dB
  {
  GLCD.DrawLine( 64, 62, st1, 10, WHITE ) ;
  GLCD.DrawLine( 63, 62, st1 -1, 10, WHITE ) ;

  GLCD.DrawLine( 64, 62, st2, 10, BLACK ) ;
  GLCD.DrawLine( 63, 62, st2 -1, 10, BLACK ) ;

  st1 = st2;
  }   
}

void setup()
{
  Serial.begin(115200); 
  GLCD.Init();   
  if(GLCD.Height >= 64)   
    icon = ArduinoIcon64x64;  // the 64 pixel high icon
  else
    icon = ArduinoIcon64x32;  // the 32 pixel high icon

  introScreen();
  GLCD.ClearScreen();
  GLCD.SelectFont(System5x7, BLACK);

  adc_Offst = analogRead(A5);       

  Draw_Table();
 
/* Setup ADC */
        ADMUX    = 0x45;        // PIN 5 Analog.

ADCSRA = ((1<< ADEN)| // 1 = ADC Enable
  (0<< ADSC)| // ADC Start Conversion
  (1<<ADATE)| // 1 = ADC Auto Trigger Enable
  (0<< ADIF)| // ADC Interrupt Flag
  (0<< ADIE)| // ADC Interrupt Enable
  (1<<ADPS2)|
  (0<<ADPS1)| // ADC Prescaler : 1 MHz.
  (0<<ADPS0)); 

ADCSRB = ((1<<ADTS2)|   // Sets Auto Trigger source - Timer/Counter1 Compare Match B
  (0<<ADTS1)|
  (1<<ADTS0));

        /* Set up TIMER 1 - ADC sampler */
        TIMSK0 = 0x00;
        TIMSK1 = 0x00;
        TIMSK2 = 0x00;

        TCCR1A = 0;
        TCCR1B = 0;
        TCCR1C = 0;

        TCCR1A =  ((1<<WGM11) | (1<<WGM10));       // Mode 15, Fast PWM
        TCCR1B =  ((1<<WGM13) | (1<<WGM12));       // Mode 15, Fast PWM

        TCCR1B |=  (1<<CS10);                      // clk/1 prescaling.
        OCR1A  = SMP_TMR1;
        OCR1B  = SMP_TMR1;

        TCNT1  = 0;
        TIFR1   |= (1<<OCF1B);
        TIMSK1  |= (1<<OCIE1B);
}

void  loop()

  char incomingByte;
  int32_t temp;
   
  temp = ppm_Level;                   // Take a copy, so Real Value not affected by calculation.
  temp = sqrt(temp);

  rms_Level = 20.0 * log10(temp +1);  // Calculated, available over Serial

  Draw_Arrow( temp );

   if (Serial.available() > 0) {   
    incomingByte = Serial.read();
    // "x" command - DEBUG
    if (incomingByte == 'x') {
       Serial.println("\n\t");
       Serial.println(adc_Offst, DEC);
       Serial.println(ppm_Level, DEC);
       Serial.println(rms_Level, 2);
      }
    if (incomingByte == 'c') {
      GLCD.ClearScreen();
      Draw_Table();
      }
   }
}

void countdown(int count){
  while(count--){  // do countdown 
    countdownArea.ClearArea();
    countdownArea.print(count);
    delay(1000); 
  } 
}

void introScreen(){ 
  GLCD.DrawBitmap(icon, 32,0);
  countdown(3);
  GLCD.ClearScreen();
  GLCD.SelectFont(Arial_14);   
  GLCD.CursorToXY(GLCD.Width/2 - 44, 3);
  GLCD.print("*** VU Meter ***");
  GLCD.DrawRoundRect(8,0,GLCD.Width-19,17, 5);   
  countdown(3); 
  GLCD.ClearScreen();
}
  It draw results on SFE  12864 display. I didn't implement HPF, and simply measuring  offset after restart

dlloyd

@Jiggy-Ninja, yep that's right (forgot to mention using 2 rectifier diodes). Anyways, usually most of the power would be consumed at low frequencies (bass) - something to consider for thermal/overload protection.
Nice project - good luck!

DVDdoug

#18
Mar 11, 2014, 12:14 am Last Edit: Mar 11, 2014, 12:26 am by DVDdoug Reason: 1
If you are not hearing distortion...   If the amplifier is not distorting and the speaker is not distorting, you are probably OK.   Realistically, most home hi-fi speakers don't get blown unless there is a party with alcohol where nobody cares about distortion.   Under "normal" home listening, I wouldn't even worry about using a 100W amp with 25W speakers because it's just not going to get turned-up that loud.    In a more professional setting, the amplifier is usually (approximately) matched to amplifier power and it's not turned-up into distortion, and everything is OK.

The specs for home/consumer speakers are often useless.    Professional speakers are rated per IEC standards, which specify a particular frequency spectrum.    (Most of the energy is assumed to be in the low-mid frequency range so a 100W speaker does not need a tweeter than can stand 100W.) 

Even the honest IEC ratings are just guidelines because there is a lot of variability in music and program material.  And, it wouldn't make sense to give the speaker a "wost case" power specification because you'd end-up using an over-rated over-expensive speaker.

Also, normal music/voice has a peak to average ratio of about 10:1.   So, if you are running an amplifier at full power, you are getting about 1/10th as much power (heat) into the speaker.    A speaker rated at 100W, is generally designed to be used with an amplifier rated at 100W or less.   But it's not going to survive with constant 100W test tones, and that's assuming the 100W power rating is honest.     

This JBL Paper basically says:
For undistorted music, you can use an amplifier with twice the speaker's ratings.

For highly distorted music, use a speaker rated for twice the amplifier's rating.
   (But, even that is not a total guarantee that you won't burn-out your speakers...  It means you are probably safe with normal-distorted program material.)

And, be aware that if you have a 2-way or 3-way speaker, you can fry the tweeter with maybe 1/10th to 1/5th of the speaker's rated power if you apply constant test-tones.   Or if you buy a 20W tweeter, it's generally rated for the high-frequency part of regular audio program material, and you can probably fry it with constant 20W test tones.

Jiggy-Ninja

i've made some adjustments to the calculation code based on Magician's code.

Code: [Select]
volatile long ADC_count = 0;

volatile long running_average_sum = 0;
volatile int running_average_count = 0;
volatile int DC_level = 512;
#define AVERAGE_MAX_COUNTS 500

#define RMS_BUFFER_SIZE 25
volatile int rms_ring_buffer[RMS_BUFFER_SIZE];
volatile int rms_buffer_index;

#define SPEAKER_IMPEDANCE_MILLIOHM 8000UL

const int mute_pin = 7;

void setup()
{
  pinMode( mute_pin, OUTPUT );
 
  // **********************************************
  // Start reading of amplifier's DC level
  // **********************************************
  // Mute amp output.
  digitalWrite( mute_pin, HIGH );
  long dc_sum = 0;
 
  // Change analog reference to Internal, to match freerunning mode.
  analogReference( INTERNAL );
  analogRead(0);
  // Speaker output needs time to settle.
  delay(500);
  for( int i=0; i<100; i++ )
  {
    dc_sum += analogRead(0);
  }
  DC_level = dc_sum / 100;
  digitalWrite( mute_pin, LOW );
  // **********************************************
  // End Reading amplifier's DC level
  // **********************************************
 
  init_freerunning_ADC();
 
  start_ADC_conversions();
  delay( 50 );
  Serial.begin( 115200 );
  Serial.print( "DC Level: " );
  Serial.println( DC_level );
 
}

void loop()
{
  static unsigned long timer = 0;
  unsigned long t_now_ms = millis();
 
  if( t_now_ms - timer > 1000 )
  {
    timer+=1000;
   
    noInterrupts();
    int rms_average = average_array( rms_ring_buffer, RMS_BUFFER_SIZE );
    unsigned long temp = ADC_count;
    ADC_count = 0;
    interrupts();
   
    Serial.print( "RMS: " );
    Serial.println( rms_average );
    Serial.print( "RMS (mV): " );
    int speaker_rms = adc_raw_to_mv(rms_average);
    Serial.println( speaker_rms );
    Serial.print( "Output (mW): " );
    Serial.println( (unsigned long)speaker_rms * speaker_rms / SPEAKER_IMPEDANCE_MILLIOHM );
    Serial.print( "ADC passes: " );
    Serial.println( temp );
    Serial.println();
  }
}

ISR(ADC_vect)
{
  ADC_count++;
 
  int result = ADC;
  result = result - DC_level;
  running_average_sum += (long)result * result;
 
  running_average_count++;
  if( running_average_count > AVERAGE_MAX_COUNTS )
  {
    rms_ring_buffer[rms_buffer_index] = sqrt( running_average_sum / AVERAGE_MAX_COUNTS );
    increment_ring_counter( rms_buffer_index, RMS_BUFFER_SIZE );
    running_average_sum = 0;
    running_average_count = 0;
  }
}

void init_freerunning_ADC()
{
  ADCSRA =  _BV(ADEN) |  // ADC enable
            _BV(ADATE) | // ADC Auto Trigger Enable
            _BV(ADIE) | // ADC Interrupt Enable
            0x06;        // /64 prescaler, ADC does approx. 19,000 conversions per second.
         
  ADCSRB = 0; // Set ADC to Freerunning mode.
 
  ADMUX = (0x3<<6) | // 1.1 V Reference
          0;          // Channel 0
}

void start_ADC_conversions()
{
  ADCSRA |= _BV(ADSC);
}

void increment_ring_counter( volatile int &index, int size )
{
  index++;
  if( index>=size )
  {
    index = 0;
  }
}

#define ADC_REF_MVOLTS 1100UL
#define VOLTAGE_DIVIDER 11
int adc_raw_to_mv( int raw )
{
  int result = raw * ADC_REF_MVOLTS / 1024;
  return result * VOLTAGE_DIVIDER;
}

int average_array(volatile int array[], int size)
{
  long sum = 0;
  for( int i=0; i<size; i++ )
    sum += array[i];
   
  return sum/size;
}

With that adjustment, the ADC runs much closer to the 19,230 conversions per second that is the theoretical maximum with a /64 prescaler, at between 19,180 ad 19,200 times per second.

I doubt I need the RMS ring buffer too. I'll work on getting rid of that next.
Hackaday: https://hackaday.io/MarkRD
Advanced C++ Techniques: https://forum.arduino.cc/index.php?topic=493075.0

Jiggy-Ninja

Finalized the sketch:
Code: [Select]
volatile long ADC_count = 0;

volatile long running_average_sum = 0;
volatile int running_average_count = 0;
volatile int DC_level = 512;
#define AVERAGE_MAX_COUNTS 1000

#define RMS_AVERAGING_DECAY 15
volatile int rms_running_average = 0;

#define SPEAKER_IMPEDANCE_MILLIOHM 8000UL

const int mute_pin = 7;

void setup()
{
  pinMode( mute_pin, OUTPUT );
 
  // **********************************************
  // Start reading of amplifier's DC level
  // **********************************************
  // Mute amp output.
  digitalWrite( mute_pin, HIGH );
  long dc_sum = 0;
 
  // Change analog reference to Internal, to match freerunning mode.
  analogReference( INTERNAL );
  analogRead(0);
  // Speaker output needs time to settle.
  delay(500);
  for( int i=0; i<100; i++ )
  {
    dc_sum += analogRead(0);
  }
  DC_level = dc_sum / 100;
  digitalWrite( mute_pin, LOW );
  // **********************************************
  // End Reading amplifier's DC level
  // **********************************************
 
  init_freerunning_ADC();
 
  start_ADC_conversions();
  delay( 50 );
  Serial.begin( 115200 );
  Serial.print( "DC Level: " );
  Serial.println( DC_level );
 
}

#define INTERVAL 1000
void loop()
{
  static unsigned long timer = 0;
  unsigned long t_now_ms = millis();
 
  if( t_now_ms - timer > INTERVAL )
  {
    timer += INTERVAL;
   
    noInterrupts();
    int rms_average = rms_running_average;
    unsigned long temp = ADC_count;
    ADC_count = 0;
    interrupts();
   
    Serial.print( "RMS: " );
    Serial.println( rms_average );
    Serial.print( "RMS (mV): " );
    int speaker_rms = adc_raw_to_mv(rms_average);
    Serial.println( speaker_rms );
    Serial.print( "Output (mW): " );
    Serial.println( (unsigned long)speaker_rms * speaker_rms / SPEAKER_IMPEDANCE_MILLIOHM );
    Serial.print( "ADC passes: " );
    Serial.println( temp );
    Serial.println();
  }
}

ISR(ADC_vect)
{
  ADC_count++;
 
  int result = ADC;
  result = result - DC_level;
  running_average_sum += (long)result * result;
 
  running_average_count++;
  if( running_average_count > AVERAGE_MAX_COUNTS )
  {
    int rms = sqrt( running_average_sum / AVERAGE_MAX_COUNTS );
    rms_running_average = ((long)rms_running_average * (RMS_AVERAGING_DECAY-1) + rms) / RMS_AVERAGING_DECAY;
    running_average_sum = 0;
    running_average_count = 0;
  }
}

void init_freerunning_ADC()
{
  ADCSRA =  _BV(ADEN) |  // ADC enable
            _BV(ADATE) | // ADC Auto Trigger Enable
            _BV(ADIE) | // ADC Interrupt Enable
            0x06;        // /64 prescaler, ADC does approx. 19,200 conversions per second.
         
  ADCSRB = 0; // Set ADC to Freerunning mode.
 
  ADMUX = (0x3<<6) | // 1.1 V Reference
          0;          // Channel 0
}

void start_ADC_conversions()
{
  ADCSRA |= _BV(ADSC);
}

void increment_ring_counter( volatile int &index, int size )
{
  index++;
  if( index>=size )
  {
    index = 0;
  }
}

#define ADC_REF_MVOLTS 1100UL
#define VOLTAGE_DIVIDER 11
int adc_raw_to_mv( int raw )
{
  int result = raw * ADC_REF_MVOLTS / 1024;
  return result * VOLTAGE_DIVIDER;
}

int average_array(volatile int array[], int size)
{
  long sum = 0;
  for( int i=0; i<size; i++ )
    sum += array[i];
   
  return sum/size;
}

Instead of a ring buffer, I used a decaying average that is updated once enough readings have been made. This minimizes processing power and lets the ADC run at about 19,200 conversions per second, very close to the theoretical maximum rate (19,230).

I also made some current measurements with my multimeter to test roughly if my RMS calculations were accurate. With no music playing (audio source turned all the way down), the quiescent current measured in at 31.4 mA, within the range the datasheet specifies (23 typical, 37 max). With some fairly loud music playing, pushing the speaker to the limit of it's power rating, RMS wattage calculation was between 500 and 600 mW at the max. Current consumption during these peak times was about 80 mA. Subtract the 30 mA quiescent current, and 12V * 50 mA = 600 mW.

From my rough testing, it looks like my calculations are within the ballpark.

Now, if I want to move forward with this, I need a good place to find a 5-10W speaker and a box to put it in. Anyone know a place? Either online or brick-and-mortar is fine.
Hackaday: https://hackaday.io/MarkRD
Advanced C++ Techniques: https://forum.arduino.cc/index.php?topic=493075.0

Go Up