Arduino Solar Power Control System :: Member Project
When Ray installed his new solar electric system, he was excited about the ability to implement this to use solar energy for his home.
However, he was in for a sunny surprise when he discovered that he either did not have enough solar energy to supply the loads he had connected or had an excess of solar energy that was going to waste.
But Ray didn’t let this stop him at all! He decided to come up with an Arduino power control system that allowed him to have more control over just how the rays of sunshine he collected went to powering his home. Take a look below at his radiant creation!
Hi Ray! So, tell us about your project.
I designed a control system that will provide load shedding/load leveling.
The controller continually examines the amount of solar energy available and connects or disconnects loads such that the maximum amount of available solar power is being used, and thus minimizes the use of grid power, protects the battery bank from being too deeply discharged, and lengthens the battery life.
Whoa, wait a minute. What a cool idea! Why did you decide to build this?
In 2017, I purchased a 4400-watt two-phase solar electric system that could generate enough electric power to power my entire household whenever solar energy was available.
It consisted of 12 solar panels, 12 AGM batteries, an inverter, a charge controller for the batteries, and a system controller.
Unlike the more typical solar systems currently being installed, which simply generate power to be sold back to the utility company, this system was designed to provide household power even in the event of a grid power failure.
However, the delivery system did not give me sufficient control to avoid a lot of ongoing human interaction to operate the system. So, I elected to design my own controller as a ‘front end’ to the system.
That’s some great innovation there! So, tell us more about how your project works?
Well, the controller allows the optimal electrical load to run on solar power. When solar energy is not available (such as at night) all loads are normally connected to grid power and, if necessary, the batteries are recharged from grid power.
Five relays (dubbed R1 through R5) switch their respective two-phase loads between solar and grid power. Relays R1 and R2 are used to remove grid power from the load group for a fraction of a second when they are being toggled.
This prevents the possibility of creating an arc between solar and grid power.
The inverter is programmed in a mode called Auto Connect. When grid power is available at the inverter input, inversion ceases and the inverter is bypassed, powering any solar loads directly from the grid.
Thus, R4 controls the presence or absence of grid voltage at the inverter input. All eight relays are also controlled by the Controller.
There is also a timer running in the background which interrupts the loop code every minute. If any major changes in the system state have occurred since the last interrupt, appropriate action is taken.
Every 15 minutes, the solar conditions are re-evaluated, and an additional load group is placed on solar power if sufficient solar power is available, one load group is removed and placed on grid power if not, or the load is left unchanged.
Wow, that’s amazing. Can you elaborate a bit on all this information being sent to the Controller?
Inputs to the Controller consist of the following measurements: grid voltage, battery voltage, battery current, and solar energy.
The grid voltage is measured to sense grid power failures. Battery voltage I measured to sense the battery condition. Battery current is measured to sense if the battery is being charged or discharged.
Available solar energy is measured by a pyranometer.
Such a plethora of information being sent to the Controller! How exactly do you, well, control the Controller?
Operating the Controller is done via three pushbutton switches. These pushbuttons create hardware interrupts.
Thus, the loop code consists of continually checking for interrupt flags and taking appropriate actions. The MODE button allows the user access to all of the available screens.
The INCrement button allows the user to choose between the various options shown on the current screen. For example, in the MODE screen, the user chooses another screen that they desire to view.
In the MAN screen, the user chooses which relay he wishes to toggle. The SELect button allows the user to select the choice made via the INC button.
Wow! Could you also tell us more about how the users interact with your program?
Six different LCD screens are available to the user. In the MAN screen, the eight relays are directly controlled.
This MANual mode is intended only for handling problem conditions that could occur. The AUTO screen displays the load groups currently being powered by solar.
The intent is that the controller will remain in this mode without human intervention for very long periods of time (months). It is designed to handle the presence or absence of grid power, low battery conditions, etc., without human intervention.
The other screens are the CONDition screen in which current system conditions are displayed, the HISTory screen showing the number of load groups that have been connected to solar power during the last 10 hours, the SHUTDOWN screen that provides a well-behaved shutdown procedure, and the MODE screen which allows the user to switch between different screens.
So, if there is a manual mode, can you talk about how the AUTO mode works as well?
The AUTO mode is where the controller is intended to operate essentially all the time, without interruption, without regard to variations in operating conditions. It is designed to operate in a variety of conditions.
The three basic system parameters are
- The presence or absence of Grid Power.
- The presence or absence of Solar Energy.
- The battery voltage condition, whether above or below desired levels.
Every 15 minutes the auto mode routine is executed.
If solar is available, the controller decides whether to add or remove a load group from solar or to leave the load as is, based on whether the batteries are currently being charged or discharged. That way, the use of solar power is maximized without needlessly discharging the batteries.
Every minute, system conditions are examined to see if any of the three basic system parameters have changed. If so, auto mode is executed and the 15-minute cycle clock is reset.
Were there some aspects of this project that you struggled with?
Well, the simplicity of the operation of this device does come at a cost.
The software which implements all the various possibilities of pushbutton combinations is quite involved and required about 800 lines of Arduino/C++ code.
Congratulations on completing such a task! What other components did you use in your project?
The controller is implemented by an Arduino MEGA2560 microcomputer programmed in Arduino/ C++ language.
It consists of the MEGA2560, a 40 character by 4-line LCD display, three push buttons, an on/off switch, and all of the necessary signal conditioning and interface circuitry.
Arduino code:
Ray was kind enough to share his source code with us. You can see his code listed below:
//This sketch implements the software-based, LCD version of the solar controller. //Written by M. Ray Johnson, Beginning August 2018 until April 2019. This algorithm assumes the Inverter is //placed in the AUTO CONNECT mode. //UPDATES: //18March2019: Eliminated all unnecessary relay closures and increased the cycle time to 15 minutes to increase //relay life. #include <TimerOne.h> #include <LiquidCrystal.h> //set up the LCD const int RS=12, E1=11, E2=10, D4=7, D5=6, D6=5, D7=4; LiquidCrystal lcd1(RS, E1, D4, D5, D6, D7); //top two lines of display LiquidCrystal lcd2(RS, E2, D4, D5, D6, D7); //bottom two lines of display volatile boolean incFlag = LOW; //declare variables used in ISRs volatile boolean selFlag = LOW; volatile boolean modeFlag = LOW; volatile boolean timerFlag = LOW; volatile boolean currentSolarState = LOW; volatile boolean currentGridState = LOW; volatile boolean currentBatState = LOW; volatile boolean changeFlag = LOW; volatile boolean cycleFlag = LOW; //LOW indicates vB has dropped to vBminLow; volatile boolean vBflag = HIGH; //HIGH indicates vB has risen to vBminHigh. volatile boolean oldSolarState = LOW; volatile boolean oldGridState = LOW; volatile boolean oldBatState = LOW; volatile boolean incEnable = LOW; //LOW disables inc button interrupts volatile boolean selEnable = LOW; volatile boolean timerEnable = LOW; volatile boolean relayState[] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH}; //HIGH indicates de-energized. //For the LG relays, HIGH indicates the load is volatile byte manCursorPos = 0; //connected to GRID power; LOW to SOLAR power. volatile byte index = 0; //For G1 and G2, HIGH indicates a closed condition, volatile byte mode = 0; //connecting their respective LGs to GRID. For volatile byte horizCursorPos = 0; //the IV relay, HIGH indicates a closed condition, volatile byte solarLoadCount = 0; //connecting Grid power to the inverter input. volatile byte loadStateHistory[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; volatile byte controlState = 0; volatile byte manIndex = 0; volatile int powerSources = 0; volatile int minutes = 0; volatile int sixSec = 0; int iBint = 0; int vBint = 0; volatile float iB = 0.0; volatile float pyra = 0.0; volatile float vB = 0.0; volatile float vG = 0.0; volatile int vDir = 0; volatile int iDir = 1; volatile unsigned int eventTime = 0; const float pyraMin = 0.4; const float vGmin = 100.0; const float vBminHigh = 53.0; const float vBminLow = 48.0; String man1 = "MANUAL MODE"; //LCD screen string arrays String man2 = "Control load group relays as desired"; String man3 = "LG1 LG2 LG3 LG4 LG5 G1 G2 IV "; String man4 = " "; String man6 = "All loads on Grid Power "; String man7 = "IV cannot open until all LGs are on GRID"; String man10 = "All loads are without power"; //manSelect are relay state labels String manSelect[] = {"GRID ","GRID ","GRID ","GRID ","GRID ", "CLO ", "CLO ", "CLO "}; String mode1 = "MODE SELECT"; String mode2 = "Select one of the following modes"; String mode3 = "MAN AUTO COND HIST SHUTDWN"; String auto1 = "AUTO MODE"; String auto3 = "Grid and Solar Power are available "; String auto4 = "Grid Power failure; Solar is available "; String auto5 = "Grid Power is available; Solar is not "; String auto6 = "Grid Power failure; Solar not available "; String auto7 = "Load Group(s) on Solar: "; String auto15 = "Battery Low"; String menu5 = "CURRENT CONDITIONS"; String shutdwn1 = "PLEASE WAIT"; String shutdwn2 = "PLEASE TURN THE CONTROLLER OFF NOW"; String history1 = "SOLAR LOAD HISTORY"; const byte relay1 = 22; //Load Group 1 relay LOW is SOLAR (energized) const byte relay2 = 23; //Load Group 2 relay const byte relay3 = 24; //Load Group 3 relay const byte relay4 = 25; //Load Group 4 relay const byte relay5 = 26; //Load Group 5 relay const byte relayG1 = 27; //Grid relay for Load Groups 1,2. LOW is open (energized) const byte relayG2 = 28; //Grid relay for Load Groups 3,4,5. LOW is open (energized) const byte relayIv = 29; //Inverter Input relay. LOW is open (energized) const byte batCurrent = A10; //Battery Current 0 to 1023 counts = -25 to +25 amps. Was A4 const byte pyranometer = A7; //0 to 5.0 VDC signal. Was A0 const byte batVoltage = A8; //0 to 66.7 VDC. Was A2 const byte gridVoltage = A9; //5.12 VDC corresponds to 125 VAC (RMS). Was A3 const int arcDelayPeriod = 20; //current arc delay period in ms //Analog input A1 is apparently faulty. Move all analog //inputs to A7 thru A10 void setup() { Serial.begin(9600); //enable the computer serial monitor lcd1.begin(40,2); // set up the LCD's number of columns and rows: lcd2.begin(40,2); pinMode(4, OUTPUT); //Define digital I/O, LCD data and control lines pinMode(5, OUTPUT); pinMode(6, OUTPUT); pinMode(7, OUTPUT); pinMode(11, OUTPUT); pinMode(10, OUTPUT); pinMode(12, OUTPUT); pinMode(relay1, OUTPUT); //Relay control lines - Load Group 1 Relay pinMode(relay2, OUTPUT); //LG2 Relay pinMode(relay3, OUTPUT); //LG3 Relay pinMode(relay4, OUTPUT); //LG4 Relay pinMode(relay5, OUTPUT); //LG5 Relay pinMode(relayG1, OUTPUT); //G1 Relay pinMode(relayG2, OUTPUT); //G2 Relay pinMode(relayIv, OUTPUT); //IV Relay pinMode(pyranometer, INPUT); //Define analog inputs. 0 to 5.0 VDC signal pinMode(gridVoltage, INPUT); //0 to 125 VAC (RMS) pinMode(batVoltage, INPUT); //0 to 60 VDC pinMode(batCurrent, INPUT); //-25 to +25 amps attachInterrupt(digitalPinToInterrupt(2), isrMode, RISING); //set up 1st priority interrupt (Mode, on pin 2) attachInterrupt(digitalPinToInterrupt(3), isrIncrement, RISING); //set up 2nd priority interrupt (Inc, on pin 3) attachInterrupt(digitalPinToInterrupt(18), isrSelect, RISING); //set up 3rd priority interrupt (Select, on pin 18) Timer1.initialize(6000000); //set up 4th priority interrupt (Timer, every 6 sec) Timer1.attachInterrupt(isrTimer); modeScreen(); //launch the mode screen digitalWrite(relayG1, LOW); //open G1 digitalWrite(relayG2, LOW); //open G2 delay(arcDelayPeriod); digitalWrite(relayIv, HIGH); //close IV relayState[7] = HIGH; manSelect[7] = "CLO "; for(byte i = 0; i < 5; i++) { //de-energize all Load Group relays [This leaves all Load Group Relays digitalWrite(22 + i, HIGH); //in the GRID position, and G1, G2, and IV closed. relayState[i] = HIGH; manSelect[i] = "GRID ";} delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); //close G1 relayState[5] = HIGH; manSelect[5] = "CLO "; digitalWrite(relayG2, HIGH); //close G2 relayState[6] = HIGH; manSelect[6] = "CLO "; modeFlag = LOW; //reset all the interrupt flags incFlag = LOW; selFlag = LOW; timerFlag = LOW; cycleFlag = LOW; solarLoadCount = 0; index = 0; manIndex = 0; horizCursorPos = 0; manCursorPos = 0; checkStateChange(); //initialize the change state conditions oldSolarState = currentSolarState; oldGridState = currentGridState; oldBatState = currentBatState; changeFlag = LOW; vBflag = HIGH; minutes = 0; for(int i = 0; i < 40; i++) { //initialize history loadStateHistory[i] = 0;} controlState = 0; } //end of setup void loop() { if(modeFlag == HIGH) { //check to see if Mode Button has been pressed delay(300); //debounce modeFlag = LOW; if(mode == 0) { //if I'm in mode screen controlState = 0; modeScreen(); return;} else if(mode == 1) { //if I'm in man screen manCursorPos = horizCursorPos; //save the cursor position manIndex = index; modeScreen(); return; } else if(mode == 2) { //if I'm in auto screen modeScreen(); return; } else if(controlState == 0) { //if I'm in no control modeScreen(); return; } else if(controlState == 1) { //if I'm in man control horizCursorPos = manCursorPos; //restore the cursor position index = manIndex; manMode(); return; } else { //I'm in auto control autoMode(); return; } } else if(incFlag == HIGH) { //check to see if Increment Button has been pressed delay(300); //debounce incFlag = LOW; if(mode == 0) { //if I'm currently in Mode screen if(horizCursorPos < 28) { //move the cursor in the Mode screen horizCursorPos += 7;} else horizCursorPos = 0; lcd2.setCursor(horizCursorPos,0); //reposition the cursor index = horizCursorPos/7; //point to the current menu item return; } else { //I'm currently in Man screen if(horizCursorPos < 35) { //move the cursor in the Man screen horizCursorPos += 5;} else horizCursorPos = 0; lcd2.setCursor(horizCursorPos,1); //reposition the cursor index = horizCursorPos/5; //point to current relay return; } } else if(selFlag == HIGH) { //check to see if the Select Button has been pressed delay(300); //debounce selFlag = LOW; if(mode == 0) { //if I'm in the mode screen if(index == 0) { //if Man Mode has been selected horizCursorPos = manCursorPos; //restore the cursor position index = manIndex; manMode(); //launch manMode return; } else if(index == 1) { //if Auto Mode has been selected minutes = 0; //start the cycle clock if(solarLoadCount > 0) { //adjust IV according to solarLoadCount if(relayState[7] == HIGH) { //if IV is closed, open it relayState[7] = LOW; manSelect[7] = "OPEN "; digitalWrite(relayIv, LOW); } } else { if(relayState[7] == LOW) { //if IV is open, close it relayState[7] = HIGH; manSelect[7] = "CLO "; digitalWrite(relayIv, HIGH); } } autoMode(); //launch autoMode return; } else if(index == 2) { //if Conditions Mode has been selected conditionsScreen(); //launch conditionsScreen return; } else if(index == 3) { //History Mode has been selected historyScreen(); //launch history screen return; } else { //Shutdown has been selected shutdwnScreen(); //launch shutdown screen return;} } else { //I'm currently in the Man screen manToggle(); manCursorPos = horizCursorPos; //save the cursor position manIndex = index; return;} } else if(timerFlag == HIGH) { //check timer flag timerFlag = LOW; if(sixSec >= 10){ sixSec = 0; checkVbFlag(); checkStateChange(); if(mode == 4) conditionsScreen(); if(mode == 5) historyScreen(); if(changeFlag == HIGH) { //if the system state has changed Serial.print("State change detected. Time = "); eventTime = millis()/60000; Serial.println(eventTime); Serial.print("vB = "); Serial.println(vB); Serial.print("pyra = "); Serial.println(pyra); Serial.print("vG = "); Serial.println(vG); if(mode == 2) goto t1; else if(mode == 1) { manMode(); return; } else return; } else if(controlState != 2) return; else { printCycle(); //update the cycle display if(minutes >= 15) { //if 15 minutes have passed t1: cycleFlag = HIGH; minutes = 0; updateHistory(); autoMode(); return; } else { minutes ++; return;} } } else { sixSec ++; return;} } else return; } //end of loop //*****PUT CALLED FUNCTIONS HERE***** void modeScreen(void) { //This function clears the display, then creates the Mode Screen lcd1.clear(); lcd2.clear(); lcd1.setCursor(0,0); lcd1.print(mode1); //print "MODE SELECT" on LCD line 1 lcd1.setCursor(0,1); lcd1.print(mode2); //print LCD line 2 lcd2.setCursor(0,0); lcd2.print(mode3); //print LCD line 3 lcd2.setCursor(0,0); //turn on the blinking cursor lcd2.cursor(); lcd2.blink(); horizCursorPos = 0; index = 0; mode = 0; selEnable = HIGH; //enable select interrupts incEnable = HIGH; //enable increment interrupts timerEnable = LOW; //disable timer interrupts return; } //end of modeScreen void conditionsScreen(void) { //This function clears the dsplay, then creates the lcd1.clear(); //Conditions Screen. lcd2.clear(); lcd1.noCursor(); lcd1.noBlink(); lcd2.noCursor(); lcd2.noBlink(); lcd1.setCursor(0,0); lcd1.print(menu5); //"CURRENT CONDITIONS" printConditions(); //print conditions on LCD lines 2 and 3 readPowerSources(); lcd2.setCursor(0,1); //print power sources on line 4 if(powerSources == 3) lcd2.print(auto3); //"Grid and Solar Power are available" if(powerSources == 2) lcd2.print(auto4); //"Grid Power failure; Solar is available" if(powerSources == 1) lcd2.print(auto5); //"Grid Power is available; Solar is not" if(powerSources == 0) lcd2.print(auto6); //"Grid Power failure; Solar not available" selEnable = LOW; //disable Select Button interrupts incEnable = LOW; //disable Increment Button interrupts timerEnable = HIGH; //enable timer interrupts mode = 4; return; } //end of Conditions Screen void historyScreen(void) { //This function clears the display, then creates the lcd1.clear(); //History Screen. lcd2.clear(); lcd1.setCursor(0,0); lcd1.print(history1); //"SOLAR LOAD HISTORY" on line 1 lcd1.noCursor(); lcd1.noBlink(); lcd2.noCursor(); lcd2.noBlink(); printHistory(); //print the load history on LCD line 4 readPowerSources(); lcd2.setCursor(0,1); //print power sources on line 4 if(powerSources == 3) lcd2.print(auto3); //"Grid and Solar Power are available" if(powerSources == 2) lcd2.print(auto4); //"Grid Power failure; Solar is available" if(powerSources == 1) lcd2.print(auto5); //"Grid Power is available; Solar is not" if(powerSources == 0) lcd2.print(auto6); //"Grid Power failure; Solar not available" selEnable = LOW; //disable Select Button interrupts incEnable = LOW; //disable Increment Button interrupts timerEnable = HIGH; //enable timer interrupts mode = 5; return; } //end of historyScreen void shutdwnScreen(void) { //This function provides for a well-behaved lcd1.clear(); //shutdown process that creates the Shutdown lcd2.clear(); //screen, de-energizes all relays, invites the lcd2.noCursor(); //user to turn off the controller, and enters lcd2.noBlink(); //an endless loop. boolean b = LOW; //avoid unnecessary relay actions for(int a = 2; a < 5; a++) { if(relayState[a] == LOW) b = HIGH; } if(b == HIGH) { digitalWrite(relayG1, LOW); //Open G1 delay(arcDelayPeriod); for(int q = 0; q < 5; q++) { if(relayState[q] == LOW) { solarLoadCount--; digitalWrite(q + 22, HIGH); relayState[q] = HIGH; manSelect[q] = "GRID "; } } delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); } //Close G1 digitalWrite(relayIv, HIGH); //close IV selEnable = LOW; //disable select interrupts incEnable = LOW; //disable increment interrupts timerEnable = LOW; //disable timer interrupts s1: lcd1.setCursor(0,0); lcd1.print(shutdwn2); //LCD line 1: "PLEASE TURN THE CONTROLLER OFF NOW" delay(1000); lcd1.clear(); delay(300); goto s1; //endless loop return; } //end of Shutdown Screen void manMode(void) { //This function implements the Manual Mode. It mode = 1; //creates the appropriate screen depending on controlState = 1; //conditions. incEnable = HIGH; //enable Increment Interrupts selEnable = HIGH; //enable Select Interrupts timerEnable = HIGH; //enable Timer Interrupts readPowerSources(); lcd1.clear(); lcd2.clear(); lcd1.setCursor(0,0); lcd1.print(man1); //"MANUAL MODE" on line 1 lcd1.setCursor(0,1); //print power sources on line 2 if(powerSources == 3) lcd1.print(auto3); //"Grid and Solar Power are available" if(powerSources == 2) lcd1.print(auto4); //"Grid Power failure; Solar is available" if(powerSources == 1) lcd1.print(auto5); //"Grid Power is available; Solar is not" if(powerSources == 0) lcd1.print(auto6); //"Grid Power failure; Solar not available" lcd2.setCursor(0,0); lcd2.print(man3); //"LG1 LG2 LG3 LG4 LG5 G1 G2 IV " lcd2.setCursor(0,1); for(int p = 0; p < 8; p++) { //print current relay states on line 4 lcd2.print(manSelect[p]); } lcd2.setCursor(horizCursorPos,1); //put the cursor back where it was lcd2.cursor(); lcd2.blink(); checkVbFlag(); if(vBflag == HIGH) { lcd1.setCursor(28,0); //clear the "battery Low" message lcd1.print(" "); } else { lcd1.setCursor(28,0); lcd1.print(auto15); } //Print "Battery Low" message return; } //end of manMode void autoMode(void) { //This function implements the Auto Mode Serial.print("AUTO Mode executed. Time = "); eventTime = millis()/60000; Serial.println(eventTime); incEnable = LOW; //disable increment interrupts selEnable = LOW; //disable select interrupts timerEnable = HIGH; //enable timer interrupts, for 5 minute load checks. checkVbFlag(); //This function executes every five minutes or mode = 2; //sooner if gross system conditions change. controlState = 2; if(manSelect[5] = "OPEN ") { //if G1 is open, close it. This is in case some relayState[5] = HIGH; //idiot left G1 or G2 open when leaving MAN mode. manSelect[5] = "CLO "; digitalWrite(relayG1, HIGH); } if(manSelect[6] = "OPEN ") { //if G2 is open, close it. relayState[6] = HIGH; manSelect[6] = "CLO "; digitalWrite(relayG2, HIGH); } lcd1.clear(); //clear the auto mode Screen lcd2.clear(); lcd1.setCursor(0,0); lcd1.print(auto1); //print "AUTO MODE" on LCD line 1 horizCursorPos = 0; lcd2.noCursor(); //turn off the blinking cursor readPowerSources(); lcd2.noBlink(); lcd1.setCursor(0,1); //print power sources on line 2 if(powerSources == 3) lcd1.print(auto3); //"Grid and Solar Power are available" if(powerSources == 2) lcd1.print(auto4); //"Grid Power failure; Solar is available" if(powerSources == 1) lcd1.print(auto5); //"Grid Power is available; Solar is not" if(powerSources == 0) lcd1.print(auto6); //"Grid Power failure; Solar not available" if(vBflag == HIGH) { lcd2.setCursor(0,1); lcd2.print(man4); //clear the "Battery Low" message if(powerSources >= 1) { //if either Grid or Solar power is available if(cycleFlag == HIGH) { //if the cycle time is up cycleFlag = LOW; if(powerSources >= 2) { //if solar is available adjustLoad(); if(solarLoadCount == 0) { a3: lcd2.setCursor(0,0); lcd2.print(man6); //print "All loads on GRID ..." on LCD line 3 a4: if(relayState[7] == LOW) { //close IV relayState[7] = HIGH; manSelect[7] = "CLO "; digitalWrite(relayIv, HIGH); } a5: printCycle(); //print cycle time printIv(); //print IV relay state return; } else { a6: if(relayState[7] == HIGH) { //open IV relayState[7] = LOW; manSelect[7] = "OPEN "; digitalWrite(relayIv, LOW); } a2: lcd2.setCursor(0,0); lcd2.print(auto7); //print LCD line 3 solarLoadCount = 0; //print the load groups on solar for (int q = 0; q < 5; q++) { if(relayState[q] == LOW) { solarLoadCount++; lcd2.print(q + 1); lcd2.print(" "); } } goto a5; } } else { //solar power is not available; put all load groups on Grid a1: boolean b = LOW; //avoid unnecessary relay actions for(int a = 2; a < 5; a++) { if(relayState[a] == LOW) b = HIGH; } if(b == HIGH) { digitalWrite(relayG1, LOW); //Open G1 delay(arcDelayPeriod); for(int q = 0; q < 5; q++) { if(relayState[q] == LOW) { digitalWrite(q + 22, HIGH); relayState[q] = HIGH; manSelect[q] = "GRID "; } } delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); } //Close G1 solarLoadCount = 0; if(powerSources == 1 || powerSources == 3) goto a3; //if Grid power is available else { lcd2.setCursor(0,0); //print LCD line 3, "All loads are without power" message lcd2.print(man10); goto a4; } } } else goto a2; } else { //put LG 1,2 on Solar; put LG 2,3,4 on Grid if(relayState[0] == HIGH || relayState[1] == HIGH) { //avoid unnecessary relay actions digitalWrite(relayG1, LOW); //Open G1 delay(arcDelayPeriod); for(int q = 0; q < 2; q++) { //connect LG 1 and 2 to Solar if(relayState[q] == HIGH) { digitalWrite(q + 22, LOW); relayState[q] = LOW; manSelect[q] = "SOL "; } } solarLoadCount = 2; boolean b = LOW; //avoid unnecessary relay actions for(int a = 2; a < 5; a++) { if(relayState[a] == LOW) b = HIGH; } if(b == HIGH) { for(int q = 2; q < 5; q++) { //connect LG 3, 4, and 5 to Grid if(relayState[q] == LOW) { digitalWrite(q + 22, HIGH); relayState[q] = HIGH; manSelect[q] = "GRID "; } } } delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); } //Close G1 goto a6; } } else { lcd2.setCursor(0,1); lcd2.print(auto15); //Print "Battery Low" on LCD line 4 goto a1; } } //End of autoMode //This function changes the state of the relay in the MAN display that has been selected for toggle. //It switches the actual selected relay, and also syncs manSelect[] and RelayState[]. The global //variable "index" points to the selected relay. If any of the LG relays are toggled, G1 or G2 will //be opened momentarily to provide arc protection. Called only from manMode. void manToggle(void) { if(index < 2) { //if the selected relay is LG1 or LG2 if(manSelect[5] == "CLO ") { //if G1 is closed, open it digitalWrite(relayG1, LOW); delay(arcDelayPeriod);} relayToggle(); //Toggle the relay delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); //close G1 goto m1; } else if(index < 5) { //if the selected relay is LG3, LG4, or LG5 if(manSelect[6] == "CLO ") { //if G2 is closed, open it digitalWrite(relayG2, LOW); delay(arcDelayPeriod);} relayToggle(); //Toggle the relay delay(arcDelayPeriod); digitalWrite(relayG2, HIGH); //close G2 goto m1; } else ctrlToggle(); //if the selected relay is G1, G2, or IV, toggle it m1: lcd2.setCursor(0,0); lcd2.print(man3); //reprint LCD line 3 lcd2.setCursor(0,1); for(int p = 0; p < 8; p++) { //print current relay states on line 4 lcd2.print(manSelect[p]); } lcd2.setCursor(horizCursorPos,1); //put the cursor back where it was return; } //end of manToggle void relayToggle(void) { //This function toggles the state of the load relays, LG1 if(manSelect[index] == "GRID ") { //through LG5, pointed to by the global pointer 'index'. manSelect[index] = "SOL "; //manSelect, relayState and the actual relay are kept in sync. relayState[index] = LOW; //It is called only from manToggle. This function DOES NOT digitalWrite((22 + index), LOW); //provide arc protection which must be done in the calling solarLoadCount++; } //function. solarLoadCount is also adjusted as needed. else { manSelect[index] = "GRID "; relayState[index] = HIGH; digitalWrite((22 + index), HIGH); solarLoadCount--; } return; } //end of relayToggle void ctrlToggle(void) { //This function toggles the state of the control relay G1, if(index > 6) { //G2 or IV, pointed to by the global pointer 'index'. if(manSelect[7] == "OPEN ") { //manSelect, relayState and the actual relay are kept in manSelect[7] = "CLO "; //sync. It is called only from manToggle. relayState[7] = HIGH; digitalWrite(relayIv, HIGH); } else { manSelect[7] = "OPEN "; relayState[7] = LOW; digitalWrite(relayIv, LOW); } return; } else if(manSelect[index] == "CLO ") { //Toggle either G1 or G2 pointed to by 'index' manSelect[index] = "OPEN "; relayState[index] = LOW; digitalWrite((22 + index), LOW); } else { manSelect[index] = "CLO "; relayState[index] = HIGH; digitalWrite((22 + index), HIGH); } return; } //end of ctrlToggle void readPowerSources() { //This function identifies the current power measureVg(); //sources (Grid and Solar) eturns the global measureSolar(); //variable powerSources as follows: if(vG > vGmin && pyra >= pyraMin) powerSources = 3; //0 = no power, 1 = grid power only, else if(vG >= vGmin && pyra < pyraMin) powerSources = 1; //2 = solar power only, 3 = both grid and solar. else if(vG < vGmin && pyra >= pyraMin) powerSources = 2; else powerSources = 0; return; } //end of readPowerSource void adjustLoad(void) { //This function adjusts the number of load groups measureIb(); //connected to solar power. It is called only from if(iB > 0.0) { //function autoMode. Global variable solarLoadCount if(solarLoadCount >= 5) return; //contains the number of load groups currently else { //connected to solar power. Basis for the load increaseLoad(); //adjustment is whether or not battery current is return; } } //positive. If so, an attempt is made to add an else if(solarLoadCount == 0) return; //additional Load Group to solar power. If not, else { //a Load Group is removed. decreaseLoad(); return; } } //end of adjustLoad void increaseLoad(void) { //This function attempts to add an additional load group to int i = -1; //Solar power. Loads are always added from relay1 to relay5. c1: i++; //If solarLoadCount becomes greater than zero, IV is opened. if(i > 4) return; //if all load groups have been tried if(relayState[i] == LOW) goto c1 //if this LG relay is already connected to solar if(i < 2) { digitalWrite(relayG1, LOW); //Open G1 delay(arcDelayPeriod); digitalWrite(i + 22, LOW); //add one load group to solar, relay1 or relay2 delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); } //Close G1 else { digitalWrite(relayG2, LOW); //Open G2 delay(arcDelayPeriod); digitalWrite(i + 22, LOW); //add one load group to solar, relay3, relay4, or relay5 delay(arcDelayPeriod); digitalWrite(relayG2, HIGH); } //Close G2 manSelect[i] = "SOL "; relayState[i] = LOW; solarLoadCount ++; if(relayState[7] == HIGH) { //if IV is closed, open it relayState[7] = LOW; manSelect[7] = "OPEN "; digitalWrite(relayIv, LOW); } return; } //end of increaseLoad void decreaseLoad(void) { //This function disconnects one load group from solar. int i = 5; //Loads are always deleted from relay5 to relay1. If b1: i--; //solarLoadCount goes to zero, IV is closed. if(i < 0) return; if(relayState[i] == HIGH) goto b1; if(i < 2) { digitalWrite(relayG1, LOW); //Open G1 delay(arcDelayPeriod); digitalWrite(i + 22, HIGH); //remove one load group from solar power, relay1 or relay2 delay(arcDelayPeriod); digitalWrite(relayG1, HIGH); } //Close G1 else { digitalWrite(relayG2, LOW); //Open G2 delay(arcDelayPeriod); digitalWrite(i + 22, HIGH); //remove one load group from solar power, relay3, relay4, or relay5 delay(arcDelayPeriod); digitalWrite(relayG2, HIGH); } //Close G2 delay(arcDelayPeriod); manSelect[i] = "GRID "; //keep manSelect up to date relayState[i] = HIGH; //keep relayState up to date solarLoadCount --; if(solarLoadCount == 0) { //if all loads have been connected to Grid power if(relayState[7] == LOW) { //if IV is open, close it relayState[7] = HIGH; manSelect[7] = "CLO "; digitalWrite(relayIv, HIGH); } return; } else return; } //end of decreaseLoad void updateHistory(void) { //This function updates the load history array for(byte m = 39; m > 0 ; m--) { //adding the latest value on the left and shifting loadStateHistory[m] = loadStateHistory[m-1]; } //previous values to the right, retaining only the loadStateHistory[0] = solarLoadCount; //latest 40 values (about 3 hours worth). } //end of updateHistory void printHistory(void) { //this function updates solar connection history on lcd1.setCursor(0,1); //LCD line 2 for(byte z = 0; z < 40; z++) { lcd1.print(loadStateHistory[z]); } return; } //end of printHistory void printCycle(void) { //This function prints the cycle time (min:sec) at lcd1.setCursor(29,0); //the end of LCD line 1. lcd1.print("cycle "); lcd1.print(minutes); return; } //end of printCycle void printIv(void) { //This function prints the state of the IV relay on lcd2.setCursor(27,1); //the end of LCD line 4. lcd2.print(" "); lcd2.setCursor(27,1); lcd2.print("IVR = "); if(relayState[7] == HIGH) lcd2.print("CLOSED"); else lcd2.print("OPEN"); return;} //end of printIv void measureVg(void) { //This function measures Grid Voltage int vGint = 0; //Take 5 readings and average them for(int w = 0; w < 5; w++) { vGint = analogRead(gridVoltage) + vGint; delay(1);} vG = float(vGint/31.10); //Gives value in VAC return; } void measureSolar(void) { //This function measures solar power availability int pyraInt = 0; //Take 5 readings and average them for(int x = 0; x < 5; x++) { pyraInt = analogRead(pyranometer) + pyraInt; delay(1);} pyra = float(pyraInt)/1023.0; //pyra = 1.0; return; } void measureVb(void) { //This function measures battery voltage vBint = 0; //Take 5 readings and average them for(int y = 0; y < 5; y++) { vBint = analogRead(batVoltage) + vBint; delay(1);} vB = float(vBint)/85.2; /*if(vDir == 1) { //for test purposes vB = vB + 0.3; if(vB > 54.0) vDir = 0; } else { vB = vB - 0.3; if(vB < 46.0) vDir = 1; }*/ return; } void measureIb(void) { //Range is roughly -25 amp to +25 amps over the range iBint = 0; //Take 5 readings and average them. This is a very for(int z = 0; z < 5; z++) { //noisy meassurement. iBint = analogRead(batCurrent) + iBint; delay(1); } iBint = iBint/5; //0 to 1023. So zero current is about 512. iBint = iBint - 512; //get to zero offset iB = float(iBint) * 0.048828; //convert counts to amps iB = (0.741 * iB) - 3.244; //conform to test data. The offset was //iB = 1.0; //originally -4.444. I'm varying it to find /*if(iBflag == 1) { iB = iB + 0.2; if(iB >= 1.0) iBflag = 0; } else { iB = iB - 0.2; if(iB <= -1.0) iBflag = 1; } Serial.print("iB = "); Serial.println (iB);*/ return; } //the best balance. void checkVbFlag(void) { //This function changes vBflag depending on the measureVb(); //history of vB. if(vB >= vBminHigh) vBflag = HIGH; else if(vB < vBminLow) vBflag = LOW; else return; } void checkStateChange(void) { //this function checks the gross system state measureSolar(); //(presence of solar, grid voltage, and high battery checkVbFlag(); //voltage) and compares them with values last measured. measureVg(); //If a major change has occured, changeFlag is set HIGH if(pyra > pyraMin) currentSolarState = HIGH; //and the current LCD screen is updated to reflect these else currentSolarState = LOW; //depending on the current mode. It is called only from if(vG > vGmin) currentGridState = HIGH; //the code that services the timer ISR, and is typically else currentGridState = LOW; //executed every minute if the timer interrupt is enabled. if(vBflag == HIGH) currentBatState = HIGH; else currentBatState = LOW; changeFlag = LOW; if(currentSolarState != oldSolarState) { changeFlag = HIGH; oldSolarState = currentSolarState; } if(currentGridState != oldGridState) { changeFlag = HIGH; oldGridState = currentGridState; } if(currentBatState != oldBatState) { changeFlag = HIGH; oldBatState = currentBatState; } return; } //end of checkStateChange void printConditions(void) { //this function measures current conditions and measureVg(); //prints them on LCD lines 2 and 3 measureSolar(); measureVb(); measureIb(); lcd1.setCursor(0,1); lcd1.print("Vg = "); lcd1.print(vG,1); lcd1.print(" Vb = "); lcd1.print(vB,1); lcd1.print(" IVR = "); if(relayState[7] == HIGH) lcd1.print("CLOSED"); else lcd1.print("OPEN "); lcd2.setCursor(0,0); lcd2.print("Pyra = "); lcd2.print(pyra,2); lcd2.print(" Ib = "); lcd2.print(iB,1); return; } //end of printConditions //*****PUT ISRs HERE***** void isrMode(void) { //first priority ISR - Mode Button, never disabled modeFlag = HIGH; } void isrIncrement(void) { //second priority ISR - Increment Button incFlag = incEnable; } //raise the interrupt flag if it is enabled void isrSelect(void) { //third priority ISR - Select Button selFlag = selEnable; } //raise the interrupt flag if it is enabled void isrTimer(void) { //fourth priority ISR - Timer timerFlag = timerEnable; } //raise the timer flag if it is enabled
Ray also included his design files for his project; check those out here:
Solar Power Controller Design File
Solar Power Controller Design File – Addeendum
About Ray:
Ray has a Ph.D. in Electrical Engineering, with experience programming in several different languages.
However, this is his first time programming in C++. He is an 80-year-old retiree who developed a newfound interest in Arduino as an affordable platform to accomplish his goals.
Wow Ray, that is impressive. I have been considering a similar unit for my solar setup, which is very similar to yours,1.8kw of PV panels, 48 volt 600Ahr battery and a 5kw inverter with built in MPPT charger. The one thing I want to do differently, is to switch an optional extra load into the circuit, if the batteries reach full charge with plenty of solar input still available, such as a small aircon unit on the battery box, or an ice maker. I also want to run two inverters, a smaller 500 watt or 1 kw unit when load conditions are light. I find the 5 kw inverter is drawing nearly 2 amps from the battery all night, even without a load at all, and this is a waste of solar/battery power,whereas a smaller inverter will draw proportionally less power at low loads.
This is awesome, I live in Australia and we get rebates from the government to put up solar on our roof. Any chance you will consider releasing the lot of it as open source code and hardware? I don’t remember seeing where you lived but if you had sun like us this system would be so much better than the 1/4 of the cost you get back from putting it in the grid. So we may pay $0.28 per kw and get $0.08 back
Hi Jay, we updated the post to include Rays code and his design write up. I hope this helps!