-
Notifications
You must be signed in to change notification settings - Fork 11
/
ard.ino
339 lines (297 loc) · 10.3 KB
/
ard.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// Main NILM Arduino sketch.
//
// Continually sends the following over the serial port as utf-8 strings:
// RMS line voltage (one phase only)
// Line voltage present indicator.
// RMS current (both phases)
// Calculated real power (both phases)
// Calculated apparent power (both phases)
// Automatic gain control state (both phases). Low = 0; Mid = 1; High = 2.
//
// Automatic gain control of the analog front end is done every sample period.
//
// This code is meant to be used with the Arduino MEGA boards only and has been
// and has been tested with the MEGA 2560 R3.
//
// Copyright (c) 2022~2024 Lindo St. Angel
#include <Arduino.h>
#include <stdio.h>
#include "emonLibCM.h"
// Define MCU GPIOs for gain control bits.
constexpr byte kCt1G0 = 42; // ATMEGA2560 pin 42 (PL7) - GPIO42 - CT1 GAIN0
constexpr byte kCt1G1 = 43; // ATMEGA2560 pin 41 (PL6) - GPIO43 - CT1 GAIN1
constexpr byte kCt2G0 = 49; // ATMEGA2560 pin 35 (PL0) - GPIO49 - CT2 GAIN0
constexpr byte kCt2G1 = 48; // ATMEGA2560 pin 36 (PL1) - GPIO48 - CT2 GAIN1
// ADC channel to signal sense mapping.
constexpr byte kVChan = 0; // Voltage sense
constexpr byte kNumAdcIChan = 2; // #ADC channels for current sensing
constexpr byte kI1Chan = 1; // Current sense 1
constexpr byte kI2Chan = 2; // Current sense 2
// Set ADC full scale range to 2.56 Volts.
constexpr double kAdcFs = 2.56;
// Analog front end gains.
constexpr double kLowGain = -1.3;
constexpr double kMidGain = -10.2;
constexpr double kHighGain = -63.0;
// Define current transformer max reading.
constexpr double kCtMax = 100.0; // 100 Amps rms.
// Define calibration constants for Emon lib power calculations.
constexpr double kVAmpCal = 198.0;
constexpr double kI1AmpCalLow = 110.9;
constexpr double kI1AmpCalMid = 14.0;
constexpr double kI1AmpCalHigh = 2.29;
constexpr double kI1PhCal = 4.6;
constexpr double kI2AmpCalLow = 110.9; // was 144.0
constexpr double kI2AmpCalMid = 14.0;
constexpr double kI2AmpCalHigh = 2.29;
constexpr double kI2PhCal = 4.2;
// Gain control structure for current transformers.
struct GainTuple {
int g1;
int g0; // LSB
};
// Gain control tuples for current transformers.
constexpr GainTuple kCt1GainCtrl = {kCt1G1, kCt1G0};
constexpr GainTuple kCt2GainCtrl = {kCt2G1, kCt2G0};
// Analog front end gain states.
enum GainState {
kLow,
kMid,
kHigh,
kInvalid
};
// Structure for AGC parameters.
struct AgcParams {
byte channel;
double v_rms;
double i_rms;
double apparent_power;
};
// Set analog gain to low.
inline void SetLowGain(GainTuple gain_control)
{
digitalWrite(gain_control.g1, LOW);
digitalWrite(gain_control.g0, LOW);
return;
}
// Set analog gain to mid.
inline void SetMidGain(GainTuple gain_control)
{
digitalWrite(gain_control.g1, LOW);
digitalWrite(gain_control.g0, HIGH);
return;
}
// Set analog gain to high.
inline void SetHighGain(GainTuple gain_control)
{
digitalWrite(gain_control.g1, HIGH);
digitalWrite(gain_control.g0, HIGH);
return;
}
// Return true if analog gain is set to low.
inline bool GetLowGain(GainTuple gain_control)
{
return (digitalRead(gain_control.g1) == LOW) && (digitalRead(gain_control.g0) == LOW);
}
// Return true if analog gain is set to mid.
inline bool GetMidGain(GainTuple gain_control)
{
return (digitalRead(gain_control.g1) == LOW) && (digitalRead(gain_control.g0) == HIGH);
}
// Return true if analog gain is set to high.
inline bool GetHighGain(GainTuple gain_control)
{
return (digitalRead(gain_control.g1) == HIGH) && (digitalRead(gain_control.g0) == HIGH);
}
// Calculate threshold for switching analog front end gain.
double GetThreshold(GainState gain_state, double voltage)
{
double threshold = kCtMax * voltage; // Max Apparent Power = max mains Irms * mains Vrms
// Adjust for analog front end gain.
switch (gain_state)
{
case kLow:
threshold /= -kLowGain;
break;
case kMid:
threshold /= -kMidGain;
break;
case kHigh:
threshold /= -kHighGain;
break;
default:
break;
}
return threshold;
}
// Get current analog front end gain setings.
GainState GetGainSettings(GainTuple gain_control)
{
if (GetLowGain(gain_control))
{
return kLow;
}
else if (GetMidGain(gain_control))
{
return kMid;
}
else if (GetHighGain(gain_control))
{
return kHigh;
}
else
{
return kInvalid;
}
}
// Automatic gain control of analog front end.
//
// Apparent power is compared against a threshold to change gain.
// The threshold is set by the maximum apparent power of the current
// transformer and mains voltage. Before a new gain is applied, a check
// is done to ensure the ADC input is not overloaded. This would cause
// incorrect readings, potentially preventing agc loop closure. The current
// channels are recalibrated each time the gain is adjusted.
GainState AutomaticGainControl(AgcParams *AgcData)
{
double v_adc;
double amp_cal_low, amp_cal_mid, amp_cal_high, phase_cal;
double threshold;
GainTuple gain_control;
GainState gain_state;
byte adc_input = AgcData->channel + 1;
// Compute ADC full scale RMS voltage, 0.905 Vrms @2.56V FS.
// NB: This is an approximation since waveforms are not pure sinusoids.
double constexpr kAdcFsVrms = kAdcFs / 2.828427125;
// Threshold scale factors for switching analog gain settings.
double constexpr kUpperThreshold = 0.95;
double constexpr kLowerThreshold = 0.85;
if (adc_input == kI1Chan)
{
amp_cal_low = kI1AmpCalLow;
amp_cal_mid = kI1AmpCalMid;
amp_cal_high = kI1AmpCalHigh;
phase_cal = kI1PhCal;
gain_control = kCt1GainCtrl;
}
else if (adc_input == kI2Chan)
{
amp_cal_low = kI2AmpCalLow;
amp_cal_mid = kI2AmpCalMid;
amp_cal_high = kI2AmpCalHigh;
phase_cal = kI2PhCal;
gain_control = kCt2GainCtrl;
}
gain_state = GetGainSettings(gain_control);
threshold = GetThreshold(gain_state, AgcData->v_rms);
if (AgcData->apparent_power > threshold * kUpperThreshold)
{
// Decrement gains.
if (gain_state == kHigh)
{
SetMidGain(gain_control);
EmonLibCM_ReCalibrate_IChannel(adc_input, amp_cal_mid, phase_cal);
}
else if (gain_state == kMid)
{
SetLowGain(gain_control);
EmonLibCM_ReCalibrate_IChannel(adc_input, amp_cal_low, phase_cal);
}
else
{/* already at low gain, do nothing */}
}
else if (AgcData->apparent_power < threshold * kLowerThreshold)
{
// Increment gains.
if (gain_state == kLow)
{
v_adc = AgcData->i_rms / amp_cal_low; // approx rms voltage at ADC input
if (v_adc * -kMidGain < kAdcFsVrms) // validity check
{
SetMidGain(gain_control);
EmonLibCM_ReCalibrate_IChannel(adc_input, amp_cal_mid, phase_cal);
}
}
else if (gain_state == kMid)
{
v_adc = AgcData->i_rms / amp_cal_mid; // approx rms voltage at ADC input
if (v_adc * -kHighGain < kAdcFsVrms) // validity check
{
SetHighGain(gain_control);
EmonLibCM_ReCalibrate_IChannel(adc_input, amp_cal_high, phase_cal);
}
}
else
{/* already at high gain, do nothing */}
}
return GetGainSettings(gain_control); // return updated gain setting
}
void setup()
{
constexpr double kAssumedVrms = 120.0; // Assumed rms line voltage when none is detected
constexpr unsigned int kLineFreq = 60; // AC line frequency in Hz
constexpr float kDataLogPeriod = 8.0; // Interval in seconds over which data is reported
// Set GPIOs as outputs to control analog front end gain.
pinMode(kCt1G0, OUTPUT);
pinMode(kCt1G1, OUTPUT);
pinMode(kCt2G0, OUTPUT);
pinMode(kCt2G1, OUTPUT);
// Set initial analog front end gains.
SetLowGain(kCt1GainCtrl);
SetLowGain(kCt2GainCtrl);
Serial.begin(115200);
// Configure Emon lib.
EmonLibCM_setAssumedVrms(kAssumedVrms);
EmonLibCM_SetADC_VChannel(kVChan, kVAmpCal); // ADC Input channel, voltage calibration
EmonLibCM_SetADC_IChannel(kI1Chan, kI1AmpCalLow, kI1PhCal); // ADC Input channel, current calibration, phase calibration
EmonLibCM_SetADC_IChannel(kI2Chan, kI2AmpCalLow, kI2PhCal); // The current channels will be read in this order
// Set ADC Reference voltage to 2.56V. INTERNAL2V56 is specific to Arduino MEGA boards and will defined by
// a macro provided by avr-gcc according to -mmcu=<device>. vscode will complain that INTERNAL2V56 is undefined
// if using a c_cpp_properties.json auto-generated by the IntelliSense configuration via the Arduino extension.
// Thus you can safely ignore this error. Note that the EmonLibCM library user manual states that only 3 values
// can be be used by this setter - VREF_EXTERNAL, VREF_NORMAL, VREF_INTERNAL - however, the code will allow for
// the use of other values because no error checking is performed.
EmonLibCM_setADC_VRef(INTERNAL2V56);
EmonLibCM_ADCCal(kAdcFs); // ADC Cal voltage
EmonLibCM_cycles_per_second(kLineFreq);
EmonLibCM_datalog_period(kDataLogPeriod);
EmonLibCM_Init(); // Start continuous monitoring
}
void loop()
{
AgcParams AgcData;
AgcParams *AgcDataPtr = &AgcData;
double i_rms, v_rms, mains_v_rms;
double apparent_power;
GainState new_gain_state;
bool ac_present; // 1 indicates mains AC present
constexpr double kACDetectThreshold = 12.0; // use assumed AC if mains less than this
if (EmonLibCM_Ready())
{
// CHeck if external AC is present, if not used assumed AC value.
mains_v_rms = EmonLibCM_getVrms();
ac_present = mains_v_rms > kACDetectThreshold;
v_rms = ac_present ? mains_v_rms : EmonLibCM_getAssumedVrms();
Serial.print(v_rms);Serial.print(",");
Serial.print(ac_present);Serial.print(",");
for (size_t i = 0; i < kNumAdcIChan; i++)
{
i_rms = EmonLibCM_getIrms(i);
apparent_power = EmonLibCM_getApparentPower(i);
Serial.print(i_rms,3);Serial.print(",");
Serial.print(EmonLibCM_getRealPower(i));Serial.print(",");
Serial.print(apparent_power);Serial.print(",");
// Run automatic gain control of analog front end.
AgcDataPtr->channel = i;
AgcDataPtr->v_rms = v_rms;
AgcDataPtr->i_rms = i_rms;
AgcDataPtr->apparent_power = apparent_power;
new_gain_state = AutomaticGainControl(AgcDataPtr);
Serial.print(new_gain_state);
// Print comma between sets of ADC data (normally two sets).
if (i < kNumAdcIChan - 1) Serial.print(",");
}
Serial.println(); // Outputs one sample.
delay(50);
}
}