daniel23
Published © LGPL

Three-Phase RMS Ammeter with Current Transformers

Display on an LCD following parameters for each phase: - the RMS current - the maximum reached - the maximum of sliding average.

AdvancedFull instructions provided1,949
Three-Phase RMS Ammeter with Current Transformers

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
Alphanumeric LCD, 20 x 4
Alphanumeric LCD, 20 x 4
×1
Current sense transformer (e.g. DL-CT1005A)
×3
Pushbutton Switch, Pushbutton
Pushbutton Switch, Pushbutton
×1
Resistors, Capacitors
×1
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
×1
Zener Single Diode, 5.6 V
Zener Single Diode, 5.6 V
×6
100mA schottky diode (e.g. BAT41 or SDM10P45)
×6

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

ac_3phase_meter_diagram_cOjXXXJRoU.pdf

Transformer board and arduino's analog input protection

Main board and display

Code

AC_3Phase_meter

Arduino
/* **********************************************************************
  True RMS current measurement on 3 phases with :
    - Current transformers (DL-CT1005A).
    - Arduino: working on all board based on the ATmega328P 
      (UNO - NANO - Pro Mini ...
    - Display Module: 20x4 Character HD44780 LCD with Backlight
      hardwired 4 or 8 bit mode or I2C (with ransom of loss of about
      212 bytes SRAM in this case).
  Depending wich one is used, comment and uncomment proper lines...

Current transformer unconnect-protection is done with two BZX84C5v6 head to tail connected. Their 120 Ohm load resistor is located near to the Arduino nano. A 1Kohm/1uF lowpass for fast transients rejection goes to the analog inputs. The nano inputs are protected by two Schottky SDM10P45 diodes connected from GND to the input and from the input to VCC. The common of transformers is connected to a divider from VCC (~+2.5V named "baseline").   Transformers are wired to the Arduino with 2 meter recycled cat.4 ethernet cable.
For the schema, see the joined file "AC_3Phase_meter Diagram.pdf"

120 mesurement are taken over 3 full cycles for each phase. The reference "baseline" is averaged and refreshed on each loop so any eventual deviation will be compensated. The measurement starts at a positive edge zero crossing. Instantaneous current is aquired over 3 cycles of mains AC and stored in an 3*120 array. The RMS value is calculated for each phase from this array and the maximum is memorized. The moving averages are calculated over about 10s and the maximums are memorized. The results and their maximums are displayed, for each phase, on a 4 lines x 20 char LCD

Many thanks to Rob Tillaart for his library "RunningAverage-0.4.0.zip" :
      https://github.com/RobTillaart/RunningAverage
      
and to Frank de Brabander for "Arduino-LiquidCrystal-I2C-library"
      https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

                                      July 30, 2021         Daniel Engel
 ************************************************************************

 Reminder: analog conversion takes about 112s (predivisor set to 128)
*/


// #include <Wire.h>
#include <LiquidCrystal_I2C.h>   // -> For I2C LCD
#include "RunningAverage.h"

#define baseLinePin 3   // Sense the mid-point of the voltage divider
#define phasePin_1 0    // Current inputs of each phase
#define phasePin_2 1    // WARNING: these labels are also used as index to fill in 
#define phasePin_3 2    // the array "rawTabVal", so they shall start at "0" 

#define testPin 13      // Change state at each interrupt (some sort of heart beat)

LiquidCrystal_I2C lcd(0x3f, 20, 4);  //  -> For I2C LCD
//LiquidCrystal_I2C lcd(0x27, 20, 4);  //  -> For I2C, alternate address
// #include <LiquidCrystal.h>           // For wired LCD (4 or 8 bit)

/* LCD wired 8 bit mode */
//const int rs = 11, en = 10, d0 = 9, d1 = 8, d2 = 7, d3 = 6, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
//LiquidCrystal lcd(rs, en, d0, d1, d2, d3,  d4, d5, d6, d7);

/* LCD wired 4 bit mode */
//const int rs = 11, en = 10, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
//LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

const uint16_t numMean = 16;        // number of values to average (16 for ~10s)
RunningAverage meanPh_1(numMean);   // constructors initialize one instance
RunningAverage meanPh_2(numMean);
RunningAverage meanPh_3(numMean);

const unsigned long sampleTime = 60000UL;   // sample over 60ms, 3 cycles for 50Hz
const unsigned long numSamples = 120UL;     // samples number to divide sampleTime exactly, low enough for the ADC
/*  Caution: strange behavior may occur if there is not enough free SRAM (heap and stack may collide).
    The first apparent flaw is the inability to reach the max average value in the display column "I_avg"

    Some tests showed if "numMean = 20", at least 400 byte of SRAM shall be free
    ( 300 should be free if "numMean = 10"  and 650 if "numMean = 40 ).
    More tests should be done to show if there is no "memory leak" or heap and stack collision over time
*/

const unsigned long sampleInterval = sampleTime / numSamples; // sampling interval, must be longer than ADC conversion time
int rawTabVal[numSamples][3];   // [LIGNES][COLONNES] (col_0: phase 1; col_1: phase 2; col_2: phase 3)

volatile bool heartbeatFlag = false;          // flag set by timer interrupt
float rmsPhase_1 = 0;           // Instantaneous RMS current (over 3 cycles)
float rmsPhase_2 = 0;
float rmsPhase_3 = 0;
float maxPh_1 = 0;              // hold the maximum reached
float maxPh_2 = 0;
float maxPh_3 = 0;
float maxAveragedPh_1 = 0; ;    // hold the Averaged maximum
float maxAveragedPh_2 = 0; ;
float maxAveragedPh_3 = 0; ;

float scale = ((float) 0.089) ; // 1/step value (in Volt)
unsigned int baseLine = 100;    // default value for tests
unsigned long previousTime;
const unsigned long timeout = 200000;
const unsigned int timer1_counter  = 34286;  //interrupt   3037; => 1s   /or/  34286; => 0.5s

void setup()
{
  noInterrupts();           // disable all interrupts
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = timer1_counter;   // preload timer
  TCCR1B |= (1 << CS12);    //  256 prescaler   (1 << CS12) | (1 << CS10);    // 1024 prescaler //
  TIMSK1 |= (1 << TOIE1);   // enable timer overflow interrupt
  interrupts();             // enable all interrupts

  pinMode(testPin, OUTPUT);

  /* This section may be commented out to save 62 bytes of SRAM */
  //
  //  Serial.begin(115200);
  //  Serial.println(__FILE__);
  //  Serial.print(F("Version: "));
  //  Serial.println(RUNNINGAVERAGE_LIB_VERSION);
  //  Serial.println(F("Test de mesures"));
  //  Serial.print(F("Default baseLine: "));
  //  Serial.print(baseLine);
  //  Serial.print(F("    "));
  //  Serial.print(F(" sampleInterval: "));
  //  Serial.print (sampleInterval);
  //  Serial.println(F(" ns "));
  //
  /*  END of section  */

  baseLine = averageMeasure(baseLinePin, 16); // use ONLY 16 or modify bitshift in "averageMeasure"

    lcd.begin();                // -> For I2C LCD
    lcd.backlight();            // -> For I2C LCD
    lcd.clear();                // -> For I2C LCD
//  lcd.begin(20, 4);               // For hard wired LCD
  lcd.setCursor(1, 0);
  lcd.print(F("Mesure courant RMS"));
  lcd.setCursor(1, 1);
  lcd.print(F("ZeroCrossing synch"));
  lcd.setCursor(4, 2);
  lcd.print(F("Scale : "));
  lcd.print(scale, 3);
  lcd.setCursor(1, 3);
  lcd.print(F("Baseline : "));
  lcd.print(baseLine);

  for (int i = 0; i < numSamples; i++) {    // Initialize the array
    rawTabVal[i][phasePin_1] = 0;
    rawTabVal[i][phasePin_2] = 0;
    rawTabVal[i][phasePin_3] = 0;
  }
  meanPh_1.clear();         // clear the instance of "RunningAverage"
  meanPh_2.clear();
  meanPh_3.clear();

  delay(3500);
  lcd.clear();
}

void loop()
{
  baseLine = averageMeasure(baseLinePin, 16); // use ONLY 16 or modify bitshift in "averageMeasure"
  /*      If not powers of 2, "bitshift" shall be replaced by "divide" in averageMeasure procedure
  */
  zeroCrossing(phasePin_1);
  aquireArrayRawValue(phasePin_1, numSamples, sampleInterval);
  zeroCrossing(phasePin_2);
  aquireArrayRawValue(phasePin_2, numSamples, sampleInterval);
  zeroCrossing(phasePin_3);
  aquireArrayRawValue(phasePin_3, numSamples, sampleInterval);

  rmsPhase_1 = rmsValue(phasePin_1, numSamples);
  rmsPhase_2 = rmsValue(phasePin_2, numSamples);
  rmsPhase_3 = rmsValue(phasePin_3, numSamples);

  if (rmsPhase_1 > maxPh_1) maxPh_1 = rmsPhase_1;
  if (rmsPhase_2 > maxPh_2) maxPh_2 = rmsPhase_2;
  if (rmsPhase_3 > maxPh_3) maxPh_3 = rmsPhase_3;
  if (heartbeatFlag)  {
    meanPh_1.addValue(rmsPhase_1);
    meanPh_2.addValue(rmsPhase_2);
    meanPh_3.addValue(rmsPhase_3);
    heartbeatFlag = false;
  }
  if ( meanPh_1.getAverage() > maxAveragedPh_1)  maxAveragedPh_1 = meanPh_1.getAverage();
  if ( meanPh_2.getAverage() > maxAveragedPh_2)  maxAveragedPh_2 = meanPh_2.getAverage();
  if ( meanPh_3.getAverage() > maxAveragedPh_3)  maxAveragedPh_3 = meanPh_3.getAverage();

  displayValue(); // could be added in the "if (heartbeatFlag)" section if the reading changes
  //           too quikly to be commfortable, the following "delay(250)" could then be removed
  delay(250);       // leads to about one measurement every half second
}


/*----------------------------------------------------------------
  Timet 1 interrupt
  ----------------------------------------------------------------
*/
ISR(TIMER1_OVF_vect)        // interrupt service routine
{
  TCNT1 = timer1_counter;   // preload timer
  digitalWrite(testPin, !digitalRead(testPin));
  heartbeatFlag = true;
}


/*----------------------------------------------------------------
  Display values and their max on the LCD
  ----------------------------------------------------------------
*/
void displayValue()
{
  // lcd.clear();
  lcd.setCursor (1, 0);
  //           12345678901234567890     // for display formatting spread over 20 characters
  lcd.print(F("I_max  I_avg  I_eff"));

  lcd.setCursor (1, 1);
  lcdPrintAlign(maxPh_1, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(maxAveragedPh_1, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(rmsPhase_1, 2, ' ');
  //    lcd.print(F(" A"));

  lcd.setCursor (1, 2);
  lcdPrintAlign(maxPh_2, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(maxAveragedPh_2, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(rmsPhase_2, 2, ' ');
  //    lcd.print(F(" A"));

  lcd.setCursor (1, 3);
  lcdPrintAlign(maxPh_3, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(maxAveragedPh_3, 2, ' ');
  lcd.print(F("  "));
  lcdPrintAlign(rmsPhase_3, 2, ' ');
  //    lcd.print(F(" A"));
}


/*----------------------------------------------------------------
  Calculation of rms value from the array "rawTabVal"
  ----------------------------------------------------------------
*/
float rmsValue(byte phase, byte number)
{
  float somme_1 = 0;
  float ValEff = 0;
  for (byte i = 0; i < number; i++)  {
    somme_1 += ((float)(rawTabVal[i][phase]) * (rawTabVal[i][phase]));
  }
  ValEff = sqrt(somme_1 / number);
  ValEff = ValEff * (float)(scale);
  return (ValEff) ;
}


/*----------------------------------------------------------------
   Acquire raw values for one phase and store them in the array
   "rawTabVal"
  ----------------------------------------------------------------
*/
void aquireArrayRawValue(byte input, byte number, uint16_t interval)
{
  for (int i = 0; i < number; i++)  {
    previousTime = micros();
    rawTabVal[i][input] = (int)(analogRead(input) - baseLine);
    //15624    digitalWrite(testPin, !digitalRead(testPin));
    while (micros() - previousTime < interval) ;
  }
}


/*----------------------------------------------------------------
   Detect the rising edge zero-crossing on any input pin
  ----------------------------------------------------------------
*/
void zeroCrossing(byte inputPin)
{
  int value;
  unsigned long zeroStartTime = micros();                                 // used for timeout if no input voltage
  value = (analogRead(inputPin) - baseLine);

  while ((int)(analogRead(inputPin) - baseLine) >= 0)  {   // wait for the negative alternation
    if (micros() - zeroStartTime > timeout) {
      Serial.print(micros());
      Serial.print(F(" timer actuel,  dmar : "));
      Serial.println(zeroStartTime);
      Serial.print(F("timeout alternance positive, entre "));   // for debug
      Serial.println(inputPin);
      break;
    }
  }
  while ((int)(analogRead(inputPin) - baseLine) < 0) {
    if (micros() - zeroStartTime > timeout) {
      Serial.print(F("timeout alternance ngative, entre "));   // for debug
      Serial.println(inputPin);
      break;
    }
  }
}


/*----------------------------------------------------------------
   Average the analog measure on any input pin on N measures.
   Give care not to exceed the variables limits while summing !!!
  ----------------------------------------------------------------
*/
uint16_t averageMeasure(byte inputPin, byte numberOfAverage)
{
  unsigned long average = 0;
  byte i = 0;
  for (i = 0; i < numberOfAverage; i++) {
    average += analogRead(inputPin);
  }
  // average = average / numberOfAverage; // bitshift is more efficient than divide...
  average = average >> 4;                 // => divide by 16
  return average;
}


/*----------------------------------------------------------------
  Serial Print the values stored in the array "rawTabVal"
    (used while debugging)
  ----------------------------------------------------------------
*/
/*
  void printArrayRawValue()
  {
  for (int i = 0; i < numSamples; i++)  {
    Serial.print (i);
    if (i < 10) Serial.print (' ');
    Serial.print (F("  R : "));
    Serial.print (rawTabVal[i][phasePin_1]);
    Serial.print(F("    S : "));
    Serial.print (rawTabVal[i][phasePin_2]);
    Serial.print(F("    T : "));
    Serial.println (rawTabVal[i][phasePin_3]);
  }
  }
  /

  /*    \\\\\\\\\\\\\\\\\\\\    END   ////////////////////    */

PrintAlign

Arduino
// *******************************************************************
// ************ LCD display 2 digits/numbers  ************************
// Display decimal ligned float number and fill missing digits
// with any character if length less than specified by "digit"
// 
//        number  : number to display
//        digit   : how much digits
//        fill    : characters to add on the left (usually "space")
// *******************************************************************
void lcdPrintAlign(float number, byte digit, char fill)
{
  switch (digit)
  {
    //    case 1:         //  1 digits not needed > goes to default
    //    goto Prior;
    //    break;
    case 2:
      goto Secundus;       //  2 digits
      break;
    case 3:
      goto Tertius;      //  3digits
      break;
    case 4:
      goto Quartus;  //  4 digits
      break;
    case 5:
      goto Quintus;   //  5 digits
      break;
    default:          //  not expected
      goto Prior;      //  but print as is
      break;
  }
Quintus:
  if (number < 10000) lcd.print(fill);
Quartus:
  if (number < 1000) lcd.print(char(fill));
Tertius:
  if (number < 100) lcd.print(char(fill));
Secundus:
  if (number + 0.005 < 10) lcd.print(char(fill)); // suite  problme d'affichage li  l'arrondi
Prior:
  lcd.print(number);
}

// *******************************************************************
// ************ Same for "serial.print  ******************************
// 
// *******************************************************************


void serialPrintAlign(uint16_t number, byte digit, char fill)
{
  switch (digit)
  {
    //    case 1:         //  1 digits not needed > goes to default
    //    goto Prior;
    //    break;
    case 2:
      goto Secundus;       //  2 digits
      break;
    case 3:
      goto Tertius;      //  3digits
      break;
    case 4:
      goto Quartus;  //  4 digits
      break;
    case 5:
      goto Quintus;   //  5 digits
      break;
    default:          //  not expected
      goto Prior;      //  but print as is
      break;
  }
Quintus:
  if (number < 10000) Serial.print(fill);
Quartus:
  if (number < 1000) Serial.print(char(fill));
Tertius:
  if (number < 100) Serial.print(char(fill));
Secundus:
  if (number < 10) Serial.print(char(fill)); 
Prior:
  lcd.print(number);
}

RunningAverage library

Arduino-LiquidCrystal-I2C-library

Credits

daniel23

daniel23

7 projects • 10 followers

Comments