/* cal_CT1_v_meter.ino * * February 2018 * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to * mimic the behaviour of a digital electricity meter. * * CT1 should be clipped around one of the live cables that pass through the * meter. The energy flow measured by CT1 is noted and a short pulse is generated * whenever a pre-set amount of energy has been recorded (normally 3600J). * * This stream of pulses can then be compared against optical pulses from a standard * electrical utility meter. The pulse rate can be varied by adjusting the value * of powerCal_grid. When the two streams of pulses are in synch, correct calibration * of the CT1 channel has been achieved. * * Robin Emley * www.Mk2PVrouter.co.uk */ #include #include #define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) // Change this value to suit the local mains frequency #define CYCLES_PER_SECOND 50 // Change this value to suit the electricity meter's Joules-per-flash rate. #define ENERGY_BUCKET_CAPACITY_IN_JOULES 3600 // definition of enumerated types enum polarities {NEGATIVE, POSITIVE}; enum LED_states {LED_ON, LED_OFF}; // active low for use at the "trigger" port which is active low // allocation of digital pins // ************************** const byte outputForLED = 4; // <-- the "trigger" port is active-low // allocation of analogue pins // *************************** const byte voltageSensor = 3; // A3 is for the voltage sensor const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale long cycleCount = 0; // used to time LED events, rather than calling millis() int samplesDuringThisMainsCycle = 0; // General global variables that are used in multiple blocks so cannot be static. // For integer maths, many variables need to be 'long' // boolean beyondStartUpPhase = false; // start-up delay, allows things to settle long energyInBucket_long; // in Integer Energy Units long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. int phaseCal_grid_int; // to avoid the need for floating-point maths int phaseCal_diverted_int; // to avoid the need for floating-point maths long DCoffset_V_long; // <--- for LPF long DCoffset_V_min; // <--- for LPF long DCoffset_V_max; // <--- for LPF // for interaction between the main processor and the ISRs volatile boolean dataReady = false; volatile int sampleI_grid; volatile int sampleI_diverted; volatile int sampleV; // For an enhanced polarity detection mechanism, which includes a persistence check #define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets enum polarities polarityOfMostRecentVsample; enum polarities polarityConfirmed; enum polarities polarityConfirmedOfLastSampleV; // For a mechanism to check the continuity of the sampling sequence #define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles int sampleCount_forContinuityChecker; int sampleSetsDuringThisMainsCycle; int lowestNoOfSampleSetsPerMainsCycle; // for control of the LED at the "trigger" port (port D4) enum LED_states LED_state; boolean LED_pulseInProgress = false; unsigned long LED_onAt; // Calibration values //------------------- // Two calibration values are used: powerCal and phaseCal. // // powerCal is a floating point variable which is used for converting the // product of voltage and current samples into Watts. // // The correct value of powerCal is dependent on the hardware that is // in use. For best resolution, the hardware should be configured so that the // voltage and current waveforms each span most of the ADC's usable range. For // many systems, the maximum power that will need to be measured is around 3kW. // // My sketch "RawSamplesTool.ino" provides a one-shot visual display of the // voltage and current waveforms as recorded by the processor. This provides // a simple way for the user to be confident that their system has been set up // correctly for the power levels that are to be measured. // // In the case of 240V mains voltage, the numerical value of the input signal // in Volts is likely to be fairly similar to the output signal in ADC levels. // 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal // output range. Stated more formally, the conversion rate of the overall system // for measuring VOLTAGE is likely to be around 1 ADC-step per Volt. // // In the case of AC current, however, the situation is very different. At // mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which // has a peak-to-peak range of 35A. This value is numerically smaller than the // likely output signal from the ADC when measuring current by a factor of // approximately twenty. The conversion rate of the overall system for measuring // CURRENT is therefore likely to be around 20 ADC-steps per Amp. // // When calculating "real power", which is what this code does, the individual // conversion rates for voltage and current are not of importance. It is // only the conversion rate for POWER which is important. This is the // product of the individual conversion rates for voltage and current. It // therefore has the units of ADC-steps squared per Watt. Most systems will // have a power conversion rate of around 20 (ADC-steps squared per Watt). // // powerCal is the RECIPR0CAL of the power conversion rate. A good value // to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) // const float powerCal_grid = 0.0446; // for CT1 const float powerCal_diverted = 0.05; // for CT2 <-- not used in this sketch // phaseCal is used to alter the phase of the voltage waveform relative to the // current waveform. The algorithm interpolates between the most recent pair // of voltage samples according to the value of phaseCal. // // With phaseCal = 1, the most recent sample is used. // With phaseCal = 0, the previous sample is used // With phaseCal = 0.5, the mid-point (average) value in used // // Values ouside the 0 to 1 range involve extrapolation, rather than interpolation // and are not recommended. By altering the order in which V and I samples are // taken, and for how many loops they are stored, it should always be possible to // arrange for the optimal value of phaseCal to lie within the range 0 to 1. When // measuring a resistive load, the voltage and current waveforms should be perfectly // aligned. In this situation, the calculated Power Factor will be 1. // const float phaseCal_grid = 1.0; const float phaseCal_diverted = 1.0; void setup() { pinMode(outputForLED, OUTPUT); delay (100); LED_state = LED_ON; // to mimic the behaviour of an electricity digitalWrite(outputForLED, LED_state); // meter which starts up in 'sleep' mode delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor Serial.begin(9600); Serial.println(); Serial.println("-------------------------------------"); Serial.println("Sketch ID: cal_CT1_v_meter.ino"); Serial.println(); // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the // scaling of the energy detection mechanism that is in use. This avoids the need // to re-scale every energy contribution, thus saving processing time. This process // is described in more detail in the function, allGeneralProcessing(), just before // the energy bucket is updated at the start of each new cycle of the mains. // // For the flow of energy at the 'grid' connection point (CT1) capacityOfEnergyBucket_long = (long)ENERGY_BUCKET_CAPACITY_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); energyInBucket_long = 0; // When using integer maths, calibration values that have supplied in floating point // form need to be rescaled. // phaseCal_grid_int = phaseCal_grid * 256; // for integer maths phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths // Define operating limits for the LP filter which identifies DC offset in the voltage // sample stream. By limiting the output range, the filter always should start up // correctly. DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin Serial.print ("ADC mode: "); Serial.print (ADC_TIMER_PERIOD); Serial.println ( " uS fixed timer"); // Set up the ADC to be triggered by a hardware timer of fixed duration ADCSRA = (1< 0) { polarityOfMostRecentVsample = POSITIVE; } else { polarityOfMostRecentVsample = NEGATIVE; } confirmPolarity(); if (polarityConfirmed == POSITIVE) { if (polarityConfirmedOfLastSampleV != POSITIVE) { // This is the start of a new +ve half cycle (just after the zero-crossing point) cycleCount++; if (beyondStartUpPhase) { // a simple routine for checking the performance of this new ISR structure if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } // Calculate the real power and energy during the last whole mains cycle. // // sumP contains the sum of many individual calculations of instantaneous power. In // order to obtain the average power during the relevant period, sumP must first be // divided by the number of samples that have contributed to its value. // // The next stage would normally be to apply a calibration factor so that real power // can be expressed in Watts. That's fine for floating point maths, but it's not such // a good idea when integer maths is being used. To keep the numbers large, and also // to save time, calibration of power is omitted at this stage. Real Power (stored as // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. // long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts // Next, the energy content of this power rating needs to be determined. Energy is // power multiplied by time, so the next step is normally to multiply the measured // value of power by the time over which it was measured. // Instanstaneous power is calculated once every mains cycle. When integer maths is // being used, a repetitive power-to-energy conversion seems an unnecessary workload. // As all sampling periods are of similar duration, it is more efficient simply to // add all of the power samples together, and note that their sum is actually // CYCLES_PER_SECOND greater than it would otherwise be. // Although the numerical value itself does not change, I thought that a new name // may be helpful so as to minimise confusion. // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than // the actual energy in Joules. // long realEnergy_grid = realPower_grid; // Energy contributions from the grid connection point (CT1) are summed in an // accumulator which is known as the energy bucket. The purpose of the energy bucket // is to mimic the operation of the supply meter. Most meters generate a visible pulse // when a certain amount of forward energy flow has been recorded, often 3600 Joules. // For this calibration sketch, the capacity of the energy bucket is set to this same // value within setup(). // // The latest contribution can now be added to this energy bucket energyInBucket_long += realEnergy_grid; // when operating as a cal program if (energyInBucket_long > capacityOfEnergyBucket_long) { energyInBucket_long -= capacityOfEnergyBucket_long; registerConsumedPower(); } if (energyInBucket_long < 0) { digitalWrite(outputForLED, LED_ON); // to mimic the nehaviour of an electricity meter energyInBucket_long = 0; } // continuity checker sampleCount_forContinuityChecker++; if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) { sampleCount_forContinuityChecker = 0; Serial.println(lowestNoOfSampleSetsPerMainsCycle); lowestNoOfSampleSetsPerMainsCycle = 999; } // clear the per-cycle accumulators for use in this new mains cycle. sampleSetsDuringThisMainsCycle = 0; sumP_grid = 0; check_LED_status(); } else { // wait until the DC-blocking filters have had time to settle if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) { beyondStartUpPhase = true; sumP_grid = 0; sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle lowestNoOfSampleSetsPerMainsCycle = 999; Serial.println ("Go!"); } } } // end of processing that is specific to the first Vsample in each +ve half cycle } // end of processing that is specific to samples where the voltage is positive else // the polatity of this sample is negative { if (polarityConfirmedOfLastSampleV != NEGATIVE) { // This is the start of a new -ve half cycle (just after the zero-crossing point) // which is a convenient point to update the Low Pass Filter for DC-offset removal // The portion which is fed back into the integrator is approximately one percent // of the average offset of all the Vsamples in the previous mains cycle. // long previousOffset = DCoffset_V_long; DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); cumVdeltasThisCycle_long = 0; // To ensure that the LPF will always start up correctly when 240V AC is available, its // output value needs to be prevented from drifting beyond the likely range of the // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. // if (DCoffset_V_long < DCoffset_V_min) { DCoffset_V_long = DCoffset_V_min; } else if (DCoffset_V_long > DCoffset_V_max) { DCoffset_V_long = DCoffset_V_max; } } // end of processing that is specific to the first Vsample in each -ve half cycle } // end of processing that is specific to samples where the voltage is negative // processing for EVERY set of samples // // First, deal with the power at the grid connection point (as measured via CT1) // remove most of the DC offset from the current sample (the precise value does not matter) long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; // phase-shift the voltage waveform so that it aligns with the grid current waveform long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); // long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when // phaseCal is not in use // calculate the "real power" in this sample pair and add to the accumulated sum long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) sampleSetsDuringThisMainsCycle++; // store items for use during next loop cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries } // ----- end of main Mk2i code ----- void confirmPolarity() { /* This routine prevents a zero-crossing point from being declared until * a certain number of consecutive samples in the 'other' half of the * waveform have been encountered. */ static byte count = 0; if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { count++; } else { count = 0; } if (count > PERSISTENCE_FOR_POLARITY_CHANGE) { count = 0; polarityConfirmed = polarityOfMostRecentVsample; } } int freeRam () { extern int __heap_start, *__brkval; int v; return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); } void registerConsumedPower() { LED_onAt = cycleCount; LED_state = LED_ON; digitalWrite(outputForLED, LED_state); LED_pulseInProgress = true; } void check_LED_status() { if (LED_pulseInProgress == true) { if (cycleCount > (LED_onAt + 2)) // normal pulse duration { LED_state = LED_OFF; digitalWrite(outputForLED, LED_state); LED_pulseInProgress = false; } } }