// Mk2a is a revised version of my stand-alone sketch for diverting suplus
// PV power to a dump load using a triac. The original Mk2 version, which includes
// more explanation and 'debug' code to support off-line working, may be found at
// http://openenergymonitor.org/emon/node/841
//
// General aspects of Mk2a:
// The Mk2a code is suitable for hardware that has multiple voltage references, such
// as emonTx. Mk2a uses integer maths for improved speed. It also uses low-level
// instructions for ADC operations which 'frees up' time that was previously unavailable
// for general processing activities. Further information about this enhancement may be
// found immediately before loop().
//
// Specific releases of Mk2a:
// The initial release of Mk2a had twin high-pass filters, as used in the standard OEM
// V&I sketch. Due to a couple of minor bugs, it was soon replaced by the _rev2 version.
//
// The _rev3 version marks a further change to the way that the raw sample streams are
// processed. This has come about because, for the purpose of calculating "real power",
// there is no need for DC offset to be pre-removed from the raw current samples. The
// standard calculation for real power does this automatically when applied over each whole
// cycle of the mains. Half-cycle processing of power has been discontinued.
// A single LPF, which is updated just once per mains cycle, has been introduced for
// the purpose of determining the DC offset of the voltage waveform. This value is then
// subtracted from each raw voltage sample. A nominal value of 512 is subtracted from
// each current sample, this being to prevent the system becoming over-sensitive to any
// imbalance conditions or random noise; the actual value is unimportant.
//
// The ANTI_FLICKER option was also introduced in _rev3. This mode uses two thresholds
// for the energy bucket rather than just one. When surplus power is available,
// the power-diversion logic operates the triac between these two limits on a hysteresis
// basis. To ensure that the anti-flicker requirement is always met, a minimum amount of
// time has to elapse between consecutive activations of the triac. Under certain
// conditions, this may cause a small amount of surplus power to be lost when the bucket
// overflows. The on-board LED (pin 13) shows whenever then this occurs. Accurate
// calibration of the system is essential when operating in this mode.
//
// TALLYMODE is a #define option which allows energy data to be collected for
// subsequent display. In _rev2, energy values in positive and negative half-cycles was
// recorded separately. From _rev3 onwards, energy is recorded for whole cycles only.
// While running in TALLYMODE, the code is fully functional. It pauses after a
// user-selectable period so that stored data can be retrieved. To avoid overflow, the
// maximum recommended duration is 10 minutes. To start again, press the 'g' key, then
// carriage-return.
// TALLYMODE provides a very easy way to calibrate the system. Clip the CT around
// one core of a cable that supplies a known load, and do a short run with appropriate
// Min and Max values. If the peak distribution (in Watts) is not where you expect it
// to be, then just change the value of powerCal until it is.
// Full details about calibration are provided just before setup().
//
// SPEEDCHECK is another #define option. When #included, the results of a built-in
// checker facility are displayed. This facility keeps track of the number of times
// that general processing fails to complete within the duration of the associated
// ADC conversion. SPEEDCHECK may be left on permanently, if desired.
// If additional workload is ever to be added, SPEEDCHECK will provide a sensitive
// indication of any undesirable effect on the underlying code. Any value other than
// zero will reduce the accuracy of power/energy calculations.
//
// Mk2a_rev3a. This corrects a minor but long-standing error in the data produced by
// Tallymode. Tallymode data is now displayed in Watts, rather than in Tally numbers.
//
// Robin Emley (calypso_rae on Open Energy Monitor Forum)
// June 2013
/*
Circuit for monitoring pulses from a supply meter using checkLedStatus():
----------------> +5V
|
/
\ 8K2
/
|
---------------------> dig 2
| |
--> / |
--> \ _
LDR / - 0.01uF
| |
----------------------> GND
*/
// There are two #define options which may be activiated independently
//
//#define TALLYMODE // for recording energy data. Comment out for normal operation
//#define SPEEDCHECK // to display the results of a built-in checker every few seconds
enum operatingModes {NORMAL, ANTI_FLICKER};
enum operatingModes operatingMode = NORMAL; // <-- select the desired mode here
#define CYCLES_PER_SECOND 50
#define JOULES_PER_WATT_HOUR 3600 // 0.001 kWh = 3600 Joules
enum polarities {NEGATIVE, POSITIVE};
enum triacStates {ON, OFF}; // the external trigger device is active low
// general global variables
const byte outputPinForLed = 13; // digital
const byte outputPinForTrigger = 9; // digital
const byte ledDetectorPin = 2; // digital
const byte ledRepeaterPin = 10; // digital
const byte voltageSensorPin = 2; // analogue
const byte currentSensorPin = 1; // analogue
const byte startUpPeriod = 5; // in seconds, to allow HP filters to settle
const int DCoffset_I = 512; // for calculating "real power", a nominal value is fine
long cycleCount = 0;
long cycleCountAtLastActivation = 0;
int samplesDuringThisCycle;
enum triacStates nextStateOfTriac;
enum triacStates triacState;
boolean triggerNeedsToBeArmed;
boolean beyondStartUpPhase = false;
float safetyMargin_WattsPerHalfCycle;
long triggerThreshold_long;
long energyInBucket_long = 0; // for integer maths
int phaseCal_int; // for integer maths
long capacityOfEnergyBucket_long; // for integer maths
long energyThreshold_long; // for integer maths
long antiFlicker_lowerEnergyThreshold;
long antiFlicker_upperEnergyThreshold;
long sumP;// for integer maths
long DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale
long DCoffset_V_min;
long DCoffset_V_max;
float minTimeBetweenActivations = 3; // <-- anti-flicker requirement (seconds)
int minCycleCountsBetweenActivations;
// items to allow general processing tasks to be efficiently partitioned.
boolean newHalfCycleFlag;
boolean voltageOKToArmTriggerFlag;
enum polarities polarityNow;
long instP;
long sampleVminusDC_long;
// values that need to be stored from one loop to the next
long lastSampleVminusDC_long; // for phaseCal algorithm
enum polarities polarityOfLastSampleV; // for zero-crossing detection
long cumVdeltasThisCycle_long; // <<--- for LPF
int sampleV_forNextLoop; // for deferred processing
int sampleI_forNextLoop; // for deferred processing
// items for LED monitoring
byte ledState, prevLedState;
boolean ledRecentlyOnFlag = false;
unsigned long ledOnAt;
long energyInBucket_4led_long = 0;
// for the in-built mechanism to check whether the ADC conversion time is ever exceeded
boolean speedFlag;
long FlagNotClearedCounter = 0;
long loopCountForSpeedChecker = 0;
long maxLoopCountForSpeedChecker = 20000; // how often to display results (~ 4500 loops/sec)
#ifdef TALLYMODE
#define NUMBER_OF_TALLIES 100
unsigned int tally[NUMBER_OF_TALLIES + 2]; // For recording the energy content in whole mains cycles.
unsigned int tallymode_maxCycleCount; // the cycleCount value when recording should cease
int tallymode_maxVal; // the maximum power to be recorded (Watts)
int tallymode_minVal; // the minimum power to be recorded (Watts)
//long tallymode_minVal_long; // x256 version for integer maths
float tallymode_stepVal; // the power increment between consecutive tallies (Watts)
//long tallymode_stepVal_long; // x256 version for integer maths
boolean tallymode_firstLoop = true;
unsigned int tallymode_noOfValuesTallied; // overflows after 10.9 minutes
unsigned long tallymode_noOfSamplePairs; // to show the average samples-per-mains-cycle rate
int tallymode_durationOfRecording; // in seconds
#endif
// Calibration values
//-------------------
// Three calibration values are required: powerCal, voltageCal and phaseCal.
// With most hardware, the default values are likely to work fine without
// need for change. A full explanation of each of these values now follows:
//
// 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 entirely 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 "MinAndMaxValues.ino" provides a good starting point ideal for
// system setup. First arrange for the CT to be clipped around either core of a
// cable which supplies a suitable load; then run the tool. The resulting values
// should sit nicely within the range 0-1023. To allow some room for safety,
// a margin of around 100 levels should be left at either end. This gives a
// output range of around 800 ADC levels, which is 80% of its usable range.
//
// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the
// voltage and current waveforms. This provides an easy way for the user to be
// confident that their system has been set up correctly for the power levels
// that are to be measured.
//
// The ADC has an input range of 0-5V and an output range of 0-1023 levels.
// The purpose of each input sensor is to convert the measured parameter into a
// low-voltage signal which fits nicely within the ADC's input range.
//
// 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 (RMS).
//
// 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 is smaller than the output signal by
// around a factor of twenty. The conversion rate of the overall system for
// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp.
//
// When measuring 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), so
// is a good default value to start with.
//
// 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 = 0.067; // <---- powerCal value
// As mentioned above, the conversion rate for AC voltage has units of
// ADC-steps per Volt. Athough not required for measuring power, this
// conversion rate does need to be known in order to determine when the
// voltage level is suitable for arming the external trigger device.
//
// To determine the voltage conversion rate, note the Min and Max values that
// are seen when measuring 240Vac via the voltage sensor. Subtract one from
// the other to find the range. Then put that value into the voltageCal formula
// below. voltageCal has units of ADC-steps per Volt.
//
// 679 is the peak-to-peak range of a 240V RMS signal. The (float) typecast
// is to prevent integer rounding
//
const float voltageCal = 656 / (float)679; // <-- the first number is the output range of
// your ADC when 240V AC is being measured
// 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 Power Factor will be 1.
//
// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct
// value of phaseCal for any hardware configuration.
//
const float phaseCal = 1.0;
void setup()
{
Serial.begin(9600);
pinMode(outputPinForTrigger, OUTPUT);
pinMode(outputPinForLed, OUTPUT);
Serial.println();
Serial.println("starting new run ...");
Serial.println();
Serial.print("DCoffset_V_long = ");
Serial.println(DCoffset_V_long);
// When using integer maths, calibration values that have supplied in floating point
// form need to be rescaled.
//
phaseCal_int = phaseCal * 256; // for integer maths
// When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the
// voltage conversion rate that is in use. This avoids the need to re-scale every
// energy contribution, thus saving processing time. To avoid integer rounding,
// energy is also recorded at x50 scale.
// This process is described in more detail in the function, processAllOtherTasks(),
// just before the bucket is updated at the start of each new whole-cycle of the mains.
//
capacityOfEnergyBucket_long = (JOULES_PER_WATT_HOUR * 50L) * (1/powerCal);
Serial.print("powerCal = ");
Serial.print(powerCal,4);
Serial.println(" Watts per ADCstep^2");
Serial.print("energy bucket = 3600 * 50 * (1/");
Serial.print(powerCal,4);
Serial.print(") = ");
Serial.print(capacityOfEnergyBucket_long);
Serial.println(" energy measurement units");
Serial.println();
energyThreshold_long = capacityOfEnergyBucket_long * 0.5; // for normal operation
antiFlicker_lowerEnergyThreshold = capacityOfEnergyBucket_long * 0.1; // for anti-flicker mode
antiFlicker_upperEnergyThreshold = capacityOfEnergyBucket_long * 0.9; // for anti-flicker mode
minCycleCountsBetweenActivations =
minTimeBetweenActivations * 50; // cycleCount increments every 20mS
if (operatingMode == NORMAL) {
energyInBucket_long = 0.8 * energyThreshold_long; } // for faster start-up
triggerThreshold_long = 50 * 256L / voltageCal;
// +50V is a suitable point in the rising waveform to arm the zero-crossing trigger.
// The 256 is because filteredV is scaled at x256 when integer maths is used.
// The reciprocal of voltageCal converts ADClevels into Volts, as described above.
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
}
// --------------------------------------------------------------------------------------
// Each time around loop(), a new pair of V & I samples is taken. Rather than using the
// normal analogRead() statement, the ADC is instructed using low-level instructions.
// By this means, the ADC's conversion time can be used to process data that was collected
// during the >>PREVIOUS<< iteration of loop().
//
// Thanks to the use of integer maths, general processing has been greatly speeded up.
// General processing has also been split into two sections, each of which can fit nicely
// into one of the periods while ADC conversions are taking place. The ADC pre-processor
// is now spending all of its time doing back-to-back V and I conversions, and the main
// Arduino is comfortably able to do all of its processing activities during these
// 'hidden' ADC conversion periods. The amount of delay that can be attributed to
// general processing activities is now ... NIL :-)
//
// The first ADC conversion is for current, that's because the phase of this waveform is
// generally slightly advanced relative to the voltage. While this first ADC conversion
// is taking place, all of the standard per-loop processing activities are done.
//
// The second ADC conversion is for voltage, that's because this waveform is generally
// slightly retarded relative to the current. While this second ADC conversion is taking
// place, all of the other general processing activities are done. This includes the
// updating of the energy bucket, which now occurs twice per mains cycle, and also the
// arming of the zero-crossing trigger.
//
// While restructuring the general processing code, some additional flags have been devised
// so that the overall workload can be partitioned more effectively. Some variables that
// were previously defined as 'locals' within loop() have now become globals.
//
void loop()
{
speedFlag = true;
ADMUX = 0x40 + currentSensorPin;
ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for CURRENT
{
processPerLoopTasks(); // all general per-loop processing
delayMicroseconds(10); // for speed checks only
}
while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;}; // wait for ADC to complete
sampleI_forNextLoop = ADC;
FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
speedFlag = true;
ADMUX = 0x40 + voltageSensorPin;
ADCSRA |= (1<<ADSC); // instruct ADC to read the sensor for VOLTAGE
{
processAllOtherTasks(); // all other processing activities
delayMicroseconds(10); // for speed checks only
}
while(bit_is_set(ADCSRA, ADSC)) {speedFlag = false;}; // wait for ADC to complete
sampleV_forNextLoop = ADC;
FlagNotClearedCounter+= speedFlag; // if the ADC conversion time has been exceeded, it's noted
#ifdef SPEEDCHECK // this is only processed if needed
loopCountForSpeedChecker++;
if (loopCountForSpeedChecker == maxLoopCountForSpeedChecker)
{
Serial.println(FlagNotClearedCounter);
loopCountForSpeedChecker = 0;
FlagNotClearedCounter = 0;
}
#endif
} // end of loop()
void processPerLoopTasks()
{
// all tasks that are required for each loop have been grouped into this routine
#ifdef TALLYMODE
tallymode_checks();
#endif
int sampleI = sampleI_forNextLoop; // extract samples taken on previous loop. This is for clarity
int sampleV = sampleV_forNextLoop; // only; the stored values won't change during this routine.
// remove DC offset from the raw voltage sample by subtracting the accurate value as determined by a LP filter.
sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long;
// remove most of the DC offset from the current sample (the precise value does not matter)
long sampleIminusDC_long = ((long)(sampleI-DCoffset_I))<<8;
// phase-shift the voltage waveform to align with the current(when a resistive load is used)
long phaseShiftedSampleVminusDC_long = lastSampleVminusDC_long
+ (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_int)>>8);
// determine the instantaneous power content, for use later
long filtV_div4 = phaseShiftedSampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6)
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)
// determine polarity, to save time later
if(sampleVminusDC_long > 0) {
polarityNow = POSITIVE; }
else {
polarityNow = NEGATIVE; }
// Check for the start of a new mains cycle
if (polarityNow != polarityOfLastSampleV) {
newHalfCycleFlag = true; }
else {
newHalfCycleFlag = false; }
if(sampleVminusDC_long > triggerThreshold_long) {
voltageOKToArmTriggerFlag = true; }
else {
voltageOKToArmTriggerFlag = false; }
// store items that need to be carried over to the next loop
// lastSampleV = sampleV; // required for HPF
// lastSampleI = sampleI; // required for HPF
// lastFilteredV_long = filteredV_long; // required for HPF
lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm
// lastFilteredI_long = filteredI_long; // required for HPF
polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries
}
void processAllOtherTasks()
{
if (newHalfCycleFlag == true)
{
if (polarityNow == POSITIVE)
{
// This is the start of a new +ve half cycle (just after the zero-crossing point)
cycleCount++;
triggerNeedsToBeArmed = true;
// If required, this is a good place from which to call checkLedStatus()
// Calculate the real power and energy during the last whole mains cycle.
//
// sumP contains the product of filtered V_ADC and I_ADC samples, just as for Mk2 and
// many other sketches. Because only integer maths is being used for Mk2a, things have
// to be scaled differently from this point.
//
// sumP contains the sum of many individual calculations of instant 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. realPower_long is
// therefore (1/powerCal) times larger than the actual power in Watts.
//
long realPower_long = sumP / samplesDuringThisCycle; // proportional to Watts
// Next, the energy content of this power rating needs to be determined. Energy is
// power divided by time, so the next step is normally to divide by the time over which
// the power was measured. For the _rev3 version, power is measured every whole
// mains cycle, so that's 50 times per second. When integer maths is being used, this
// stage seems unnecessary. As all sampling periods are of similar duration (20mS), it
// is more efficient simply to add all the power samples together, and note that their
// sum is actually 50 times greater than it would otherwise be.
//
// Although the numerical value itself does not change, I think a new name would be
// helpful so as to avoid any confusion. The'energy' variable below is 50 * (1/powerCal)
// times larger than the actual energy in Joules.
//
long realEnergy_long = realPower_long;
// Energy contributions are summed in an accumulator which is generally known as the
// energy bucket. The purpose of the energy bucket is to mimic the operation of the
// supply meter. Most supply meters have a range of 0.001kWh within which energy can
// pass to and fro without loss or charge to the user. The energy bucket in the Mk2
// Power Router was therefore to 0.001kWh, or 3600 Joules. Moreover, its contents
// were correctly pre-scaled to be in Joules.
//
// As described above, energy contributions for the Mk2a version are scaled somewhat
// higher that for its floating point predecessor. The capacity of the energy bucket for
// Mk2a _rev3 thereore needs to be 3600J * 50 * (1/powerCal). This is the value that
// appears in setup().
//----------------------------------------------------------------------------------
// WARNING - Serial statements can interfere with time-critical code, use with care!
//----------------------------------------------------------------------------------
/*
if(((cycleCount % 50) == 5) && polarityNow == POSITIVE) // display once per second
{
Serial.print(realPower_long);
Serial.print(", ");
Serial.print(energyInBucket_long);
Serial.print(", ");
Serial.println(energyInBucket_4led_long); // has no upper or lower limits
energyInBucket_4led_long = 0; // useful for calibration/test purposes
}
*/
if (beyondStartUpPhase)
{
// Providing that the initial settling time has passed,
// add this latest contribution to the energy bucket
energyInBucket_long += realEnergy_long;
energyInBucket_4led_long += realEnergy_long;
// Apply max and min limits to bucket's level. This is to ensure correct operation
// when conditions change, i.e. when import changes to export, and vici versa
//
if (energyInBucket_long > capacityOfEnergyBucket_long)
{
energyInBucket_long = capacityOfEnergyBucket_long;
digitalWrite(outputPinForLed, 1); // illuminate the on-board LED if bucket overflows
}
else
{
digitalWrite(outputPinForLed, 0); // clear the on-board LED if bucket is not overflowing
if (energyInBucket_long < 0)
{
energyInBucket_long = 0;
}
}
#ifdef TALLYMODE
tallymode_updateData(realPower_long); // update the relevant tally
#endif
}
else
{
// check whether the system had time to settle
if(cycleCount > (startUpPeriod * CYCLES_PER_SECOND))
{
beyondStartUpPhase = true;
// Serial.println ("go!");
}
}
// clear the per-cycle accumulators for use in this new mains cycle.
samplesDuringThisCycle = 0;
sumP = 0;
} // end of processing that is specific to the first Vsample in each +ve half cycle
else
{
// This is the start of a new -ve half cycle (just after the zero-crossing point)
//
// This is a convenient point to update the Low Pass Filter for DC-offset removal ...
long previousOffset = DCoffset_V_long;
DCoffset_V_long = previousOffset + (0.01 * cumVdeltasThisCycle_long);
cumVdeltasThisCycle_long = 0;
// ... and prevent the LPF's output from drifting beyond the likely range of the voltage signal
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; }
// The triac can change state at each -ve going zero crossing. Update the state of a
// flag which shows the current state of the triac (for anti-flicker mode logic)
//
if (nextStateOfTriac == ON) {
triacState = ON; }
else {
triacState = OFF; }
} // end of processing that is specific to the first Vsample in each -ve half cycle
}
if (polarityNow == POSITIVE)
{
// for whole-cycle operation, the trigger is only armed during +ve half cycles
if (triggerNeedsToBeArmed == true)
{
// check to see whether the trigger device can now be reliably armed
if(voltageOKToArmTriggerFlag == true)
{
if (operatingMode == NORMAL)
{
// check to see whether the energy threshold has been reached
if (energyInBucket_long > energyThreshold_long)
{
nextStateOfTriac = ON;
}
else
{
nextStateOfTriac = OFF;
}
}
else
{
// We're in anti-flicker mode, so different logic applies ...
//
if (energyInBucket_long < antiFlicker_lowerEnergyThreshold)
{
// when below the lower threshold, always turn the triac off
nextStateOfTriac = OFF;
}
else
if (energyInBucket_long > antiFlicker_upperEnergyThreshold) // upper threshold
{
// when above the upper threshold, ensure that the triac is on if permitted
if (triacState != ON)
{
if (cycleCount > cycleCountAtLastActivation + minCycleCountsBetweenActivations)
{
nextStateOfTriac = ON; // the external trigger device is active low
cycleCountAtLastActivation = cycleCount;
}
}
}
else
{
// the energy level is between the upper and lower thresholds, so
// leave the triac's state unchanged (anti-flicker measure)
}
} // end of anti-flcker mode logic
// set the Arduino's output pin accordingly, and clear the flag
digitalWrite(outputPinForTrigger, nextStateOfTriac);
triggerNeedsToBeArmed = false;
}
}
}
else
{
// When the voltage polarity is negative, no special processing is needed.
}
// Rest of processing for ALL Vsamples
// (this has to go here because the counters / accumulators may have just been cleared)
sumP +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC)
cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter
samplesDuringThisCycle++;
}
// ----- end of main Mk2a code -----
#ifdef TALLYMODE
void tallymode_checks()
{
if (tallymode_firstLoop) {
tallymode_setup(); } // user-dialogue for recording energy data
else
if (cycleCount >= tallymode_maxCycleCount) {
tallymode_dispatchData(); // send recorded energy data to the Serial monitor
// cycleCount = 0;
tallymode_firstLoop = true; // get ready for another run
pause(); } // so that user can access data from the Serial monitor
else
if (beyondStartUpPhase) {
tallymode_noOfSamplePairs++; }
}
void tallymode_setup()
{
char inbuf[10];
int tempInt;
byte noOfBytes;
boolean done;
// <-- start of commented out section to save on RAM space.
/*
Serial.println ("WELCOME TO TALLYMODE ");
Serial.println ("This mode of operation allows the energy content of individual mains cycles");
Serial.println ("to be analysed. For nomal operation, the #define TALLYMODE statement");
Serial.println ("should be commented out. ");
*/
// <-- end of commented out section to save on RAM space.
Serial.println ();
Serial.println ("Tallymode setup:");
Serial.print ("Time to run (secs)? ");
done = false;
while (!done) {
noOfBytes = Serial.available();
if (noOfBytes > 0) { done = true; } else { delay(100); }}
for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
Serial.readBytes(inbuf, noOfBytes);
tempInt = atoi(inbuf); Serial.println (tempInt);
tallymode_maxCycleCount = tempInt * CYCLES_PER_SECOND;
tallymode_durationOfRecording = tempInt;
Serial.print ("Min value (Watts)? ");
done = false;
while (!done) {
noOfBytes = Serial.available();
if (noOfBytes > 0) { done = true; } else { delay(100); }}
for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
Serial.readBytes(inbuf, noOfBytes);
tempInt = atoi(inbuf); Serial.println (tempInt);
tallymode_minVal = tempInt;
// Serial.print(" tallymode_minVal = "); Serial.println(tallymode_minVal);
// tallymode_minVal_long = tallymode_minVal * (1/powerCal); // used for power, not energy,
// hence there's no x100 term.
Serial.print ("Max value (Watts)? ");
done = false;
while (!done) {
noOfBytes = Serial.available();
if (noOfBytes > 0) { done = true; } else { delay(100); }}
for (tempInt = 0; tempInt < 10; tempInt++) { inbuf[tempInt] = 0; }
Serial.readBytes(inbuf, noOfBytes);
tempInt = atoi(inbuf); Serial.println (tempInt);
tallymode_maxVal = tempInt;
// Serial.print(" tallymode_maxVal = "); Serial.println(tallymode_maxVal);
if (tallymode_maxVal <= tallymode_minVal) {
Serial.print(7); // beep
Serial.println ("ERROR!"); }
tallymode_stepVal = (float)(tallymode_maxVal - tallymode_minVal) / NUMBER_OF_TALLIES;
// Serial.print(" tallymode_stepVal = "); Serial.println(tallymode_stepVal);
/*
tallymode_stepVal_long = tallymode_stepVal * (1/powerCal); // used for power, not energy,
// hence there's no x50 term.
tallymode_halfStep = (tallymode_stepVal_long / 2); // to avoid integer-rounding
*/
for (tempInt = 0; tempInt < NUMBER_OF_TALLIES + 2; tempInt++) {
tally[tempInt] = 0; }
energyInBucket_long = (capacityOfEnergyBucket_long / 2); // for rapid startup of power distribution
cycleCount = 0;
tallymode_noOfValuesTallied = 0;
tallymode_noOfSamplePairs = 0;
beyondStartUpPhase = false; // to allow LPF to re-settle for next run
tallymode_firstLoop = false;
Serial.print(" Recording will start in ");
Serial.print(startUpPeriod);
Serial.println(" seconds ... ");
// startTime = millis();
};
void tallymode_updateData(long power_long)
{
float powerInWatts = power_long * powerCal;
int index;
if (powerInWatts < tallymode_minVal)
{
index = 0; // tally[0] is for underflow
}
else
if (powerInWatts > tallymode_maxVal)
{
index = NUMBER_OF_TALLIES + 1; // tally[N+1] is for overflow
}
else
{
index = (powerInWatts - tallymode_minVal) / tallymode_stepVal;
index += 1; // because the linear tallies run from 1 to N, not from 0
}
tally[index]++; // increment the relevant tally
tallymode_noOfValuesTallied++;
}
void tallymode_dispatchData()
{
// <<- start of commented out section, to save on RAM space!
/*
Serial.println ();
Serial.println ("Format of results: ");
Serial.print ("Sorted data runs from tally[1] (min) to tally[");
Serial.print (NUMBER_OF_TALLIES);
Serial.println ("] (max).");
Serial.print ("tally[0] is below range; tally[ ");
Serial.print (NUMBER_OF_TALLIES + 1);
Serial.println ("] is above range.");
Serial.println("Tally results are displayed with max power first. For each one:");
Serial.println(" mid-range power of tally (Watts), number of times recorded.");
Serial.println();
*/
// <<- end of commented out section, to save on RAM space!
Serial.println("Results for this run:");
Serial.print (tallymode_minVal);
Serial.println (", <- min value of tally range (W)");
Serial.print (tallymode_maxVal);
Serial.println (", <- max value of tally range (W)");
Serial.print (tallymode_stepVal);
Serial.println (", <- step value between tallies (W)");
Serial.print (NUMBER_OF_TALLIES);
Serial.println (", <- No. of tallies");
Serial.print (tallymode_durationOfRecording);
Serial.println (", <- duration (secs)");
Serial.print (tallymode_noOfValuesTallied);
Serial.println (", <- no of values tallied");
Serial.print ( tallymode_noOfSamplePairs /
((float)tallymode_durationOfRecording * CYCLES_PER_SECOND), 1);
Serial.println (", <- loops per mains cycle (av)");
Serial.println("***");
float powerVal;
for (int index = NUMBER_OF_TALLIES + 1; index >= 0; index--)
{
/*
// display the index value for this tally
Serial.print (index);
Serial.print (", ");
*/
// display the power value for this tally
if (index == NUMBER_OF_TALLIES + 1)
{
Serial.print (">");
Serial.print (tallymode_maxVal);
}
else
if (index == 0)
{
Serial.print ("<");
Serial.print (tallymode_minVal);
}
else
{
if (index == NUMBER_OF_TALLIES)
{
powerVal = tallymode_maxVal - (tallymode_stepVal / 2);
}
else
{
powerVal -= tallymode_stepVal;
}
if ((int)powerVal == powerVal)
{
Serial.print (powerVal, 0); // to suppress the decimal part for integers
}
else
{
Serial.print (powerVal); // non-integers are displayed to 2 dec places
}
}
Serial.print ("W");
// display the number of hits for this tally
Serial.print (", ");
Serial.println (tally[index]);
}
};
void pause()
{
byte done = false;
byte dummyByte;
while (done != true)
{
if (Serial.available() > 0) {
dummyByte = Serial.read(); // to 'consume' the incoming byte
if (dummyByte == 'g') done++; }
}
}
#endif
// helper function, to process LED events, can be conveniently called once per mains cycle
void checkLedStatus()
{
ledState = digitalRead (ledDetectorPin);
if (ledState != prevLedState)
{
// led has changed state
if (ledState == ON)
{
// led has just gone on
ledOnAt = millis();
ledRecentlyOnFlag = true;
}
else
{
// led has just gone off
if (ledRecentlyOnFlag == true)
{
ledRecentlyOnFlag = false;
Serial.print ("** LED PULSE ** "); // this is a chargeable event
}
else
{
Serial.print ("** LED OFF ** "); // 'no longer exporting' is also a chargeable event
}
Serial.println(millis()/1000);
}
}
else
{
// the LED state has not changed
if (ledState == ON)
{
if (ledRecentlyOnFlag == true)
{
// check to see if the known duration of a pulse has been exceeded
unsigned long timeNow = millis();
if ((timeNow - ledOnAt) > 50)
{
Serial.print ("** LED ON **"); // 'exporting' is a non-chargeable state
Serial.print (", energy in bucket = ");
Serial.println((long)(energyInBucket_4led_long));
ledRecentlyOnFlag = false;
}
}
}
}
prevLedState = ledState;
}