Thursday, January 23, 2014

Whole House Power Monitor - Part 4 - Current Calibration

The voltage calibration was fairly easy, since it only has to work over a fairly narrow range, nominal 120V, plus or minus perhaps 10V.  

Current calibration covers a much wider range, from no load to max load.  The linearity and residual noise of the hardware made it impossible to get good results using the Arduino map() function, with only two calibration points.  I tried various combinations of no load or a small load for the lower calibration point; and various loads for the upper calibration point, but could never get good accuracy across the whole range, from no load to full load. 

In hindsight, I probably should have incorporated some better signal conditioning, but that's water under the bridge.  I was bound and determined to find a software solution, and in the end it worked out fine.

I created two current ranges in the software.  The lower current range covers no load to about 250W, and the upper range covers 250W to full load.  The cal standards for the lower range were a 75W light bulb and a 250W light bulb.  And for the upper range, I used the same 250W light bulb and a 1000W heat gun.  For reference I used the Kill-A-Watt current and power readings, also correlated the current readings with a Craftsman Current Clamp Meter.

After calibration, I spot checked a bunch of different loads.  On the low end, I used a 25W lightbulb.  It measured pretty low, about 15W, but I can live with that.  No load does read zero, so that's good.  I measured several loads ranging from 75W to 1000W, and got pretty good accuracy, within a few percent.

But I wanted to measure something much more substantial, and figured out a way to use a Weller soldering gun to create about 24A.  That's up in the range I expect my main breakers are carrying on average.  As shown in this YouTube video, the results were pretty good.

Regarding max load, the current clamps are rated for 600A, my main breakers are 200A, and I expect my max current will be under 50A.  The current clamps have a switch to select either 1mV/A, or 10mV/A, and I'm using the 10mV/A setting.  So at 50A, the current probe output will be 0.5V.  That's RMS, so the peak to peak will be about 1.4V.  Perhaps a bit more than that, depending on the shape of the current waveform.  The input range of the Moteino is 3.3V, so I think I've got plenty of headroom.

Here's the final sketch, with the calibration constants:


const char* NAME = "Moteino_Main_Tx ";
const char* VERSION = "1.414 ";
const char* DATE = "1/25/2014 ";

// Moteino_Main_Tx (using LowPowerLab RFM12b library)
// www.mikesmicromania.com
// LED on D9
// Phase 1 current sense to A1
// Phase 2 current sense to A2
// Phase 1 voltage sense to A3
// Phase 2 voltage sense to A4
// 60 Hz INT to D3 (INT1)
// Serial and Tx output @ 1Hz: ASCII string, e.g.
// "MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

#include <PString.h>   //arduiniana.org/libraries/PString/
#include <RFM12B.h>    //www.lowpowerlab.com

#define ShowRawA2D false // set to true for calibration measurements

// cal factors for converting raw A2D values to volts, amps, watts
// initial default values map 1 to 1 output raw A2D values 0 to 1023
const float CFrawV1low  = 214.0; // raw A2D value
const float CFrawV1high = 251.5;
const float CFcalV1low  = 110.0; // scaled Volts
const float CFcalV1high = 130.0; 

const float CFrawV2low  = 214.0; // raw A2D value
const float CFrawV2high = 251.5;
const float CFcalV2low  = 110.0; // scaled Volts
const float CFcalV2high = 130.0; 
  
const float CFrawA1low  = 2.75; // A2D value, 68W load
const float CFrawA1mid  = 7.28; // A2D value, 250W load
const float CFrawA1high = 27.7; // A2D value, 970W load
const float CFcalA1low  = .645; // Amps, 68W load
const float CFcalA1mid  = 2.26; // Amps, 250W load
const float CFcalA1high = 8.77; // Amps, 970W load

const float CFrawA2low  = 2.65; // A2D value, 68W load
const float CFrawA2mid  = 7.18; // A2D value, 250W load
const float CFrawA2high = 27.7; // A2D value, 970W load
const float CFcalA2low  = .645; // Amps, 68W load
const float CFcalA2mid  = 2.26; // Amps, 250W load
const float CFcalA2high = 8.77; // Amps, 970W load 

// radio settings
const int NODEID    =  3; // this node ID
const int GATEWAYID =  1; // send to node 1: Moteino Rx R-Pi
const int NETWORKID = 99; // Rx must be on same group to receive
const int myCS = 10;      // RFM12B chip select, SPI SS. 
                          // Pin 9 on my UNO shields. 10 on JeeNodes, Moteinos.

RFM12B radio;             // object, instance of class RFM12B 

// HW pin assignments
const int LED    = 9; // activity LED, 1 Hz
const int LEDBIT = 1; // activity LED, PORTB
const int A1SENS = 1; // phase 1 current: A1
const int A2SENS = 2; // phase 2 current: A2
const int V1SENS = 3; // phase 1 voltage: A3
const int V2SENS = 4; // phase 2 voltage: A4
const int HzSENS = 3; // IRQ = D3 (60Hz from Q1 collector)

// global constants
const int HzINT  = 1; // INT1, pin D3
const int MaxN  = 65; // max # readings (good down to 48 Hz)
const int NAVG = 30;  // average over 30 cylces, 0.5 seconds

void setup () { 
  radio.Initialize(NODEID, RF12_433MHZ, NETWORKID, 0, 8, RF12_2v75, myCS);
  pinMode(HzSENS, INPUT);      // D3 is IRQ1, active low 
  digitalWrite(HzSENS, HIGH);  // enable internal pullup
  pinMode(LED, OUTPUT);        // D9 drives LED to ground
  digitalWrite(LED, LOW);      // LOW =  LED off
  // speed up ADC to minimize phase error measuring voltage and current
  // arduino cookbook 2nd ed, pg 624
  bitClear(ADCSRA, ADPS0);
  bitClear(ADCSRA, ADPS1);
  bitSet(ADCSRA, ADPS2);  
  Serial.begin(115200);
  Serial.print(NAME);
  Serial.print(VERSION);
  Serial.println(DATE); 
}

volatile int unsigned measState; // state machine, clocked by line frequency

void loop () {
  //Serial.println("loop()");
  int V_1[MaxN], A_1[MaxN], V_2[MaxN], A_2[MaxN];  // waveform arrays
  int KAW_N = 0;
  float KAW_V1=0, KAW_A1=0, KAW_V2=0, KAW_A2=0, KAW_W1=0, KAW_W2=0;
  for(int i = 0; i < NAVG; i++){
    // acquire voltage and current samples
    int n = 0;
    if(i > 0)
      measState = 1; // 1 is partial half cycle
    else
      measState = 0; // first attachInterrupt take longer
    
    attachInterrupt(HzINT, AC60HZ, FALLING);
    while(measState <= 1);  // skip cycle 0 and 1
    //digitalWrite(LED,HIGH);
    //bitSet(PORTB, LEDBIT);
    while((measState <= 2) && (n < MaxN)) { // measure 1 cycle
      //digitalWrite(LED,HIGH);
      //bitSet(PORTB, LEDBIT);     // 0
      A_1[n] = analogRead(A1SENS); // 16.8 us
      V_1[n] = analogRead(V1SENS); // 16.8 us
      A_2[n] = analogRead(A2SENS); // 16.8 us
      V_2[n] = analogRead(V2SENS); // 16.8 us
      //digitalWrite(LED,LOW);
      //bitClear(PORTB, LEDBIT);   // 68 us
      n++;
      // time difference between voltage and current readings is about 20.4 us
      // 16.8 us is about 0.44 degrees at 60 Hz, good enough!
      // target 50 quad-readings over 1 cycle @ 60Hz
      // total reading time: 4 * 16.8 us * 50 = 3.4 ms
      // total dead time: (1/60) - 3.4 ms = 13.2666 ms
      // dead time between readings, for 50 readings: 
      // 13.2666 ms / 50 = 265 us
      delayMicroseconds(265); // fine tuned for 50 readings @ 60 Hz
    } 
    //digitalWrite(LED,LOW);
    //bitClear(PORTB, LEDBIT);
    detachInterrupt(HzINT);
    KAW_N += n; // number of readings
    // compute offset levels
    float V1Sum=0, A1Sum=0, V2Sum=0, A2Sum=0, W1Sum=0, W2Sum=0;
    for(int i=0; i<n; i++){
      V1Sum += V_1[i];
      A1Sum += A_1[i];  
      V2Sum += V_2[i];
      A2Sum += A_2[i];   
    }
    
    float aV1, aA1, aV2, aA2; // averages = DC mean values
    aV1 = V1Sum / n;
    aA1 = A1Sum / n;
    aV2 = V2Sum / n;
    aA2 = A2Sum / n;
    
    V1Sum = A1Sum = V2Sum = A2Sum = 0; // compute std.dev, AC RMS
    for(int i=0; i<n; i++){
      V1Sum += (V_1[i] - aV1) * (V_1[i] - aV1); // dV1^2
      A1Sum += (A_1[i] - aA1) * (A_1[i] - aA1); // dA1^2
      V2Sum += (V_2[i] - aV2) * (V_2[i] - aV2); // dV2^2
      A2Sum += (A_2[i] - aA2) * (A_2[i] - aA2); // dA2^2
      W1Sum += (V_1[i] - aV1) * (A_1[i] - aA1); // dV1*dA1
      W2Sum += (V_2[i] - aV2) * (A_2[i] - aA2); // dV2*dA2
    }
     
    KAW_V1 += sqrt(V1Sum / n);  // root(mean(V1^2))
    KAW_A1 += sqrt(A1Sum / n);  // root(mean(A1^2))
    KAW_V2 += sqrt(V2Sum / n);  // root(mean(V2^2))
    KAW_A2 += sqrt(A2Sum / n);  // root(mean(A2^2))
    KAW_W1 += W1Sum / n;        // mean(sum(dV1[i]*dA1[i]))
    KAW_W2 += W2Sum / n;        // mean(sum(dV2[i]*dA2[i]))
  }
  
  KAW_V1 /= NAVG; // average all measurements
  KAW_A1 /= NAVG;
  KAW_V2 /= NAVG;
  KAW_A2 /= NAVG;
  KAW_W1 /= NAVG;
  KAW_W2 /= NAVG;

  if(!ShowRawA2D)
  {  // apply cal factors
  
    // voltage
    KAW_V1 = map(KAW_V1, CFrawV1low, CFrawV1high, CFcalV1low, CFcalV1high); 
    KAW_V2 = map(KAW_V2, CFrawV2low, CFrawV2high, CFcalV2low, CFcalV2high); 
    
    // current & power, phase 1
    if(KAW_A1 < CFrawA1mid * .9) // use lower current range for loads under about 225 watts
    { 
      KAW_A1 = map(KAW_A1, CFrawA1low, CFrawA1mid, CFcalA1low, CFcalA1mid);
      KAW_W1 = map(KAW_W1, CFrawV1low * CFrawA1low, CFrawV1high * CFrawA1mid, CFcalV1low * CFcalA1low, CFcalV1high * CFcalA1mid);
    }
    else // use upper current range for loads above about 200W
    {
      KAW_A1 = map(KAW_A1, CFrawA1mid, CFrawA1high, CFcalA1mid, CFcalA1high);
      KAW_W1 = map(KAW_W1, CFrawV1low * CFrawA1mid, CFrawV1high * CFrawA1high, CFcalV1low * CFcalA1mid, CFcalV1high * CFcalA1high);
    }
    
    // current & power, phase 2
    if(KAW_A2 < CFrawA2mid * .9) // use lower current range for loads under about 225 watts
    { 
      KAW_A2 = map(KAW_A2, CFrawA2low, CFrawA2mid, CFcalA2low, CFcalA2mid);
      KAW_W2 = map(KAW_W2, CFrawV2low * CFrawA2low, CFrawV2high * CFrawA2mid, CFcalV2low * CFcalA2low, CFcalV2high * CFcalA2mid);
    }
    else // use upper current range for loads above about 200W
    {
      KAW_A2 = map(KAW_A2, CFrawA2mid, CFrawA2high, CFcalA2mid, CFcalA2high);
      KAW_W2 = map(KAW_W2, CFrawV2low * CFrawA2mid, CFrawV2high * CFrawA2high, CFcalV2low * CFcalA2mid, CFcalV2high * CFcalA2high);
    }
    
    // power
    //KAW_W1 = map(KAW_W1, CFrawV1lo*CFrawA1lo, CFrawV1hi*CFrawA1hi, CFcalV1lo*CFcalA1lo, CFcalV1hi*CFcalA1hi);
    //KAW_W2 = map(KAW_W2, CFrawV2lo*CFrawA2lo, CFrawV2hi*CFrawA2hi, CFcalV2lo*CFcalA2lo, CFcalV2hi*CFcalA2hi);
    
    // prevent negative values (e.g. no load current, plus noise)
    if(KAW_A1 < 0.4)
      KAW_A1 = KAW_W1 = 0;
    if(KAW_A2 < 0.4)
      KAW_A2 = KAW_W2 = 0;  
  }
  
  // format Tx string like: "Main 1500 N 120.1 V1 3.0 A1 300 W1 120.1 V2 3.0 A2 300 W2"
  char buffer[67];
  PString str(buffer, sizeof(buffer));
  str.print("Main ");
  str.print(KAW_N);
  str.print(" N ");
  str.print(KAW_V1);
  str.print(" V1 ");
  str.print(KAW_A1);
  str.print(" A1 ");
  str.print(KAW_W1);
  str.print(" W1 ");
  str.print(KAW_V2);
  str.print(" V2 ");
  str.print(KAW_A2);
  str.print(" A2 ");
  str.print(KAW_W2);
  str.print(" W2 ");
  
  //Serial.println(str);
  radio.Send(GATEWAYID, str, str.length());
  
  // flash LED so we know we're alive
  //digitalWrite(LED,HIGH);
  bitSet(PORTB, LEDBIT);   
  delay(10);
  bitClear(PORTB, LEDBIT); 
  //digitalWrite(LED,LOW);  // save power
}

void AC60HZ(){ 
  //digitalWrite(LED,HIGH);
  //bitSet(PORTB, LEDBIT);
  measState++;
  //Serial.print("measState:");
  //Serial.println(measState);
  //digitalWrite(LED,LOW);
  //bitClear(PORTB, LEDBIT);
}

// overload Arduino integer map() function to support floats, doubles
double map(double x, double in_min, double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}