Tuesday, April 16, 2013

JeeNode Kill-A-Watt: Software Walk-Through

In yesterday's post I described the JeeNode Kill-A-Watt hardware build.  Today we'll look at the software, and explain the math used and also the calibration.  For this build, I used the JeeLib library, available here.  JeeLib provides support for the RFM12B radio and the low power mode, which is essential for keeping the KAW power supply from getting bogged down.  In fact, the JeeNode spends most of its time asleep.  About once per second, it wakes up, measures voltage and current over one AC line cycle, does some calculations, transmits the results, flashes the LED, and goes back to sleep.

I'll present the entire sketch in separate post, for easy copying.  But here I'll present it in sections, explaining along the way:

// VERSION 1.414

// JeeNode_Kill_A_Watt (using JeeLib)
// works with JeeNode or Moteino.  (Moteino easier to fit inside KAW)
// www.mikesmicromania.com
// activity LED on DIO2 (Arduino Digital 5)
// Kill-A-Watt coarse current sensor to AIO1 (Arduino A0)
// Kill-A-Watt voltage sensor to AIO2 (Arduino A1)
// Kill-A-Watt frequency signal to INT (Arduino D3)
// Kill-A-Watt fine current sensor to AIO4 (Arduino A3)

#include <JeeLib.h> // jeelabs.org

#define CALMODE false // true to xmit raw values (to establish cal factors)
CALMODE should be set true during calibration, explained below.
typedef struct { // payload structure for RFM12B packet
int Status;    // 0: OK, 1: first reading, -1: low voltage
int      N;    // # of readings
float    V;    // average AC RMS voltage
float    A;    // average AC RMS current, coarse scale
float    W;    // average watts, coarse current scale
float    a;    // average AC RMS current, fine scale
float    w;    // average watts, fine current scale
} payload; // typedef

The payload struct defines the radio transmission, and the receiver software should use the same data structure.  KAW is an instance variable of type payload.

// cal factors for map()
const float CFrawVlo = 248.2; // raw value from A2D
const float CFrawVhi = 270.9;
const float CFcalVlo = 115.2; // scaled Voltage
const float CFcalVhi = 125.1;
const float CFrawAlo = 36.5;  // A2D
const float CFrawAhi = 131.9;
const float CFcalAlo = 2.18;  // Amps (coarse scale current)
const float CFcalAhi = 8.07;
const float CFrawalo = 49.7;  // A2D
const float CFrawahi = 361.0;
const float CFcalalo = 0.29;  // amps (fine scale current)
const float CFcalahi = 2.18;
I'll explain these cal factors below.
const int NODE      =  1; // this node ID
const int BROADCAST = 31; // send to node 31: broadcast
const int GROUP    = 210; // Rx must be on same group to receive
const int myCS = 10;      // RFM12B chip select, AKA SS
// 9 on my shields
// 10 on JeeNodes, Moteinos
Constants for radio settings are used in the setup() function.
// pin assignments    JeeNode   Arduino
const int LED    = 6; // DIO3 = D6
const int ASENS  = 0; // AI01 = A0 (Yellow wire from pin 1, coarse current)
const int VSENS  = 1; // AI02 = A1 (White wire from pin 14, voltage)
const int aSENS  = 3; // AI04 = A3 (Blue wire from pin 8, fine current)
const int HzSENS = 3; // IRQ  = D3 (Red wire from Q3 collector, 60Hz)
const int HzINT  = 1; //        INT1
const int MaxN  = 65; // max # readings (good down to 48 Hz)
const int NAP  = 925; // nap time in milliseconds
// boilerplate for low-power waiting
ISR(WDT_vect) { Sleepy::watchdogEvent(); }

ISR variable is required for the "Sleepy" (power save) methods.  Details on http://jeelabs.org/
void setup () {   // turn the radio off in the most power-efficient manner   Sleepy::loseSomeTime(32);   rf12_set_cs(myCS); // note this is before rf12_initialize()   rf12_initialize(NODE, RF12_433MHZ, GROUP);   rf12_sleep(RF12_SLEEP);   pinMode(HzSENS,INPUT); // IRQ1 input, D3   pinMode(LED,OUTPUT);   digitalWrite(LED,LOW); // off   // speed up ADC to minimize phase measurment error   bitClear(ADCSRA,ADPS0);   bitClear(ADCSRA,ADPS1);   bitSet(ADCSRA,ADPS2);   //wait another 2s for the power supply to settle   Sleepy::loseSomeTime(2000); }

When setup() begins, the JeeNode has just powered up, and the KAW has just come up as well.  We need to quickly initialize the radio and then turn it off ASAP.  The LED pin connects to the activity LED.  The 60 Hz square wave is connected to HzSENS pin (D3) which is INT1.  The standard analogRead function is too slow, so we speed it up by a factor of 8, to minimize the delay between measuring voltage and current.
boolean First = true; // the very 1st reading int V[MaxN], A[MaxN], a[MaxN]; // waveform arrays volatile int unsigned measState; // state machine, clocked by line frequency boolean toggle; // alternate maeasurements on + or - cycle
First is a flag, to mark the first reading after power-up.  Three arrays V[], A[], and a[] hold the waveform samples taken over one cycle.  measState is incremented by the interrupt service routine, triggered by the 60 Hz square wave.  toggle is a variable that alternates the measurement: one time it will start on a positive half cycle, the next time it will start on a negative half cycle.

Next we enter the main loop:
void loop () {
// acquire voltage and current samples
int n = 0;
measState = 0; // 0 is partial half cycle
toggle = !toggle;
if (toggle){
attachInterrupt(HzINT,AC60HZ,RISING);
}else{
attachInterrupt(HzINT,AC60HZ,FALLING);
}
while(measState < 2);              // skip partial cycle

n is the index for the waveform arrays.  MeasState starts at 0, and is incremented by either rising or falling edges of the 60 Hz square wave.  The code above skips the partial AC line cycle already in progress.  Then the code below starts at the exact beginning of a new AC line cycle:
//digitalWrite(LED,HIGH);
while(measState < 3 && n < MaxN) { // measure 1 cycle
//digitalWrite(LED,HIGH);
A[n] = analogRead(ASENS); // 20.4 us
V[n] = analogRead(VSENS); // 20.4 us
a[n] = analogRead(aSENS); // 20.4 us
//digitalWrite(LED,LOW);
n++;
// 20.4 us is about 0.44 degrees at 60 Hz
// 3 * 20.4 us * 100 = 6.12 ms
// (2/60) - 6.12 ms = 27.2 ms
// 27.2 ms / 100 = 272 us
// so delay about 272 us
delayMicroseconds(280); // fine tuned for 50 readings
}
//digitalWrite(LED,LOW);
detachInterrupt(HzINT);

The while() loop above executes over one period of the AC line cycle.  analogRead is called three times, once each for the coarse current waveform A[], the voltage waveform V[], and the fine current waveform a[].  Those reads take about 60 uS, followed by a delay of 280 uS.  This works out to 50 sets of readings, over one AC line cycle.  (The commented digitalWrites were used to flash the LED and verify the operation of this time-critical portion of the code, as described in this prior post.)
KAW.N = n; // number of readings

n is now equal to 50, and KAW.N is the first field in the radio transmission packet.
// compute offset levels   float VSum=0, ASum=0, aSum=0, WSum=0, wSum=0;   for(int i=0; i<n; i++){     VSum += V[i];     ASum += A[i];       aSum += a[i];   }   float aV, aA, aa; // averages   aV = VSum/n;   aA = ASum/n;   aa = aSum/n;
A[], V[], and a[] are the waveforms, with 50 samples each.  At this point, we don't know yet whether we'll use the coarse current scale A[] or the fine current scale a[], so we'll calculate both and decide later.

Recall that the KAW quad op-amp operates with a single supply, with a reference voltage of about 2.33V.  After the resistor divider, this becomes 2.33 x 10K/14.7k, or about 1.59 volts.  So the waveforms are centered around this level, and we want to remove that level, so we can work with the AC waveshape only.  Since we measured over exactly one cycle, we can simply compute the mean, or average level.  aV is the average voltage level. aA is the average coarse current scale.  aa is the average fine current scale.

// compute std.dev. = AC RMS
VSum = ASum = aSum = 0;
for(int i=0; i<n; i++){
VSum += (V[i]-aV)*(V[i]-aV);
ASum += (A[i]-aA)*(A[i]-aA);
aSum += (a[i]-aa)*(a[i]-aa);
WSum += (V[i]-aV)*(A[i]-aA);
wSum += (V[i]-aV)*(a[i]-aa);
}
KAW.V = sqrt(VSum/n);
KAW.A = sqrt(ASum/n);
KAW.a = sqrt(aSum/n);
KAW.W = WSum/n;
KAW.w = wSum/n;

Now we can calculate the standard deviation, which in this case is the same as the RMS, root mean square.  At each sample point, we square the difference between the sample and the mean.  We sum all those squares, find the average or mean, and then take the square root.  Thus, the (square) root of the mean of the squares (of the differences).  We do that for the V[], A[] and a[] waveforms, and while we're at it, calculate the instantaneous power, W[] and w[], at each sample, for both the fine and coarse current scales.

If the load is reactive, there will be a phase difference between the voltage and current waveforms.  The power waveforms correctly account for that phase difference.

At this point, the KAW data structure has linearized values for RMS voltage, current and power, but they are not yet scaled in volts, amps and watts.

When we want to calibrate, we set CALMODE true, and those unscaled RMS values are transmitted.  We pick them up on the receiver, and record them, along with the measurements displayed on the KAW itself.  That's how I determined the values for the cal factor constants above, and used in the next block of code:
#if not CALMODE  // apply cal factors   // voltage   KAW.V = map(KAW.V, CFrawVlo, CFrawVhi, CFcalVlo, CFcalVhi);      // coarse current scale, above 2 A   KAW.A = map(KAW.A, CFrawAlo, CFrawAhi, CFcalAlo, CFcalAhi);      // coarse wattage scale, Current above 2 A   KAW.W = map(KAW.W, CFrawVlo*CFrawAlo, CFrawVhi*CFrawAhi, CFcalVlo*CFcalAlo, CFcalVhi*CFcalAhi);      // fine current scale, below 2 A   KAW.a =  map(KAW.a, CFrawalo, CFrawahi, CFcalalo, CFcalahi);      // fine wattage scale, Current below 2 A   KAW.w = map(KAW.w, CFrawVlo*CFrawalo, CFrawVhi*CFrawahi, CFcalVlo*CFcalalo, CFcalVhi*CFcalahi);

The code above uses the familiar Arduino map() function to scale the RMS values into calibrated volts, amps and watts.  (Actually, as shown below, it's an overloaded map() function.)
// prevent negative values (e.g. no low load current, noise)   KAW.a = max(KAW.a, 0);   KAW.w = max(KAW.w, 0);   KAW.A = max(KAW.A, 0);   KAW.W = max(KAW.W, 0);

This code prevents negative values for current and power, caused by no load (0 A) and the presence of small noise signals. Note, during calibration, this code is disabled, so the negative values (if any) are displayed.  I verified that these negative values are very small and occasional, of no real consequence.

// use fine current scale for 2A or less load current
if(KAW.a <= 2.0) {
KAW.A = KAW.a;
KAW.W = KAW.w;
}
#endif

Now that we have calibrated current values, we can decide whether to use the coarse (>2A) or fine (<=2A) current scale.  The receiver doesn't have to choose, it simply uses KAW.A and KAW.W.
if (First) {
First = false;
KAW.Status = 1;          // 1st reading
} else if (KAW.V < 115 ) { // KAW analog supply sags
KAW.Status = -1;         // accuracy warning
} else {
KAW.Status = 0;          // OK
}

KAW.Status lets the receiver know if this is the first measurement.  This would be useful for example when plugging a new appliance into the KAW.  Just cycle power the KAW and Status byte will mark that event.  I also defined a status of -1 to indicate if the AC line voltage was less than 115V.  I found that the KAW analog linearity is not so good below 115V.
rf12_sleep(RF12_WAKEUP);   while (!rf12_canSend()) // wait for xmit ready     rf12_recvDone(); // advance RF12 driver state machine   rf12_sendStart(BROADCAST, &KAW, sizeof(KAW)); // send payload   rf12_sendWait(0); // wait (2 = in standby) for xmit done   rf12_sleep(RF12_SLEEP);
Finally, it's time to transmit!  The code above is a bit cryptic but gets the job done, in a couple ms.
digitalWrite(LED,HIGH); // flash LED so we know we're alive
delay(10);              // 10 ms visable
digitalWrite(LED,LOW);  // save power!

Sleepy::loseSomeTime(NAP);
}

Then all we need to do is flash the LED for 10 ms, and go to sleep.  All the code above takes about 75 ms, so we sleep for 925 ms, giving a total repetition rate of about 1 second.
Here's the interrupt handler.  All it has to do is increment measState:

void AC60HZ(){
measState++;
}
And finally, here's a new version of the map function, that works with floats instead of ints.
// overload Arduino int map() function to support floats float map(float x, float in_min, float in_max, float out_min, float out_max) {   return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }

So that's it.  I hope this code walk-through has been useful, feel free to post questions or comments.  And look to the next post for the complete sketch that you can easily copy and paste into a text file.  I'm sorry I don't know how (or if) I can attach a file to this blog.