Solar energy LED beacons

What?

Powering a grid using the sun is not as straightforward as it initially seems. This is because the voltage that a solar panel outputs is proportional to sunlight intensity, which is rarely stable (especially in the UK!). Consequently, we need a way of drawing varying amounts of power based on how much is available from a panel.

One solution is using an SMPS (Switch-Mode Power Supplies), which can draw different voltages (hence currents) based on the duty cycle of a PWM signal. The duty cycle is proportion that a signal with period T seconds is on (or HIGH), and varies between 0 and 100 percent.

This also gives us the ability to either “buck” or “boost” the input voltage from the solar panels. “Buck” is a term for reducing input voltage, and “boost” means increasing the voltage. We have decided to go with the boost configuration as this allows us to charge the capacitor to a higher voltage.

The diagram below outlines the circuit’s layout:

HTML5 Icon

Equipment

How?

In the boost SMPS, we have implemented a type of MPPT algorithm named “perturb and observe”. This is explained in the code below. In the Buck SMPS, we implemented a PID algorithm that limits the current at the LEDs’ current ratings, to ensure that none of them break. The capacitor is directly connected to the grid, which means that the voltage of the grid will never change abruptly due to changing sunlight conditions.

/*
 * Boost SMPS code
 * pin6 is PWM output at 62.5kHz.
 * duty-cycle saturation is set as 2% - 98%
*/

#include <Wire.h>
#include <INA219_WE.h>

INA219_WE ina219; // this is the instantiation of the library for the current sensor

float open_loop = 0.5, closed_loop; // Duty Cycles
float va,vb,vref,iL,dutyref,current_mA; // Measurement Variables
float prev_vb,prev_iL;
float del_pwr = 0,del_vb = 0,del_iL = 0;
float vb_limit;
float variable_p;
unsigned int sensorValue0,sensorValue1,sensorValue2,sensorValue3;  // ADC sample values declaration
float curr_pwr,prev_pwr;
float ev=0,cv=0,ei=0,oc=0; //internal signals
float Ts=0.001; //1 kHz control frequency. It's better to design the control period as integral multiple of switching period.
float kpv=0.05024,kiv=15.78,kdv=0; // voltage pid.
float u0v,u1v,delta_uv,e0v,e1v,e2v; // u->output; e->error; 0->this time; 1->last time; 2->last last time
float kpi=0.02512,kii=39.4,kdi=0; // float kpi=0.02512,kii=39.4,kdi=0; // current pid.
float u0i,u1i,delta_ui,e0i,e1i,e2i; // Internal values for the current controller
float uv_max=4, uv_min=0; //anti-windup limitation
float ui_max=50, ui_min=0; //anti-windup limitation
float current_limit = 1.0;
boolean Boost_mode = 0;
boolean CL_mode = 0;
unsigned int loopTrigger;
unsigned int com_count=0;   // a variables to count the interrupts. Used for program debugging.
float dutyCycleIncrement = 0.01;    // Duty cycle adjustment step size
int count = 0;
float pwr_threshold = 0.01;
float vb_threshold = 0.01;

void setup() {

  //Basic pin setups
  
  noInterrupts(); //disable all interrupts
  pinMode(13, OUTPUT);  //Pin13 is used to time the loops of the controller
  pinMode(3, INPUT_PULLUP); //Pin3 is the input from the Buck/Boost switch
  pinMode(2, INPUT_PULLUP); // Pin 2 is the input from the CL/OL switch
  analogReference(EXTERNAL); // We are using an external analogue reference for the ADC

  // TimerA0 initialization for control-loop interrupt.
  
  TCA0.SINGLE.PER = 999; //
  TCA0.SINGLE.CMP1 = 999; //
  TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV16_gc | TCA_SINGLE_ENABLE_bm; //16 prescaler, 1M.
  TCA0.SINGLE.INTCTRL = TCA_SINGLE_CMP1_bm; 

  // TimerB0 initialization for PWM output
  
  pinMode(6, OUTPUT);
  TCB0.CTRLA=TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; //62.5kHz
  analogWrite(6,120); 

  Serial.begin(115200);   //serial communication enable. Used for program debugging.
  interrupts();  //enable interrupts.
  Wire.begin(); // We need this for the i2c comms for the current sensor
  ina219.init(); // this initiates the current sensor
  Wire.setClock(700000); // set the comms speed for i2c
  
}

 void loop() {
  if(loopTrigger) { // This loop is triggered, it wont run unless there is an interrupt
    
    digitalWrite(13, HIGH);   // set pin 13. Pin13 shows the time consumed by each control cycle. It's used for debugging.
    
    // Sample all of the measurements and check which control mode we are in
    sampling();
    CL_mode = digitalRead(3); // input from the OL_CL switch
    Boost_mode = digitalRead(2); // input from the Buck_Boost switch

    if (Boost_mode){
      if (CL_mode) { //Closed Loop Boost
          pwm_modulate(1); // This disables the Boost as we are not using this mode
      }else{ // Open Loop Boost
          current_limit = 2; // 
          oc = iL-current_limit; // Calculate the difference between current measurement and current limit
          if ( oc > 0) {
            open_loop=open_loop+0.001; // We are above the current limit so less duty cycle
          } else {
            open_loop=open_loop-0.001; // We are below the current limit so more duty cycle
          }
          
          vb_limit = 17;
          if((vb-vb_limit) > 0){
            open_loop += 0.1; 
            }
            
          open_loop=saturation(1-open_loop,0.99,0.29); // saturate the duty cycle at the reference or a min of 0.01
          pwm_modulate(open_loop); // and send it out
          curr_pwr = iL*vb; //calculated power in this instance
          del_pwr = curr_pwr - prev_pwr; //calculate power difference
          del_vb = vb - prev_vb; // calculate voltage difference
          if(del_pwr != 0){ 
            if(del_pwr > 0){ 
              if(del_vb < 0){
                open_loop -= 0.01; 
                }
              else{
                open_loop += 0.01; 
                }
              }
            else{
              if(del_vb < 0){ 
                open_loop += 0.01; 
                }
              else{
                open_loop -= 0.01;
                }
              }
          }
          else{
            //do nothing
            }
      open_loop=saturation(1 - open_loop,0.99,0.29); // saturate the duty cycle between 0.99 and 0.29
      pwm_modulate(open_loop); // and send it out
      variable_p = prev_pwr; // for debugging
      prev_pwr = curr_pwr;
      prev_vb = vb;
      }
    }else{      
      if (CL_mode) { // Closed Loop Buck
          pwm_modulate(1); // This disables the Buck as we are not using this mode
      }else{ // Open Loop Buck
          pwm_modulate(1); // This disables the Buck as we are not using this mode
      }
    }

    com_count++;              //used for debugging.
    if (com_count >= 10) {  //send out data every second.
      Serial.print("Va: ");
      Serial.print(va);
      Serial.print("\t");

      Serial.print("Vb: ");
      Serial.print(vb);
      Serial.print("\t");

      Serial.print("Inductor Current: ");
      Serial.print(iL);
      Serial.print("\t\t");

      Serial.print("Boost Mode: ");
      Serial.print(Boost_mode);
      Serial.print("\t\t");

      Serial.print("CL Mode: ");
      Serial.print(CL_mode);
      Serial.print("\t\t");

      Serial.print("Duty(inverted): ");
      Serial.print(1-open_loop);
      Serial.print("\t\t");

      Serial.print("Curr_pwr: ");
      Serial.print(curr_pwr);
      Serial.print("\t\t");

      Serial.print("Iout: ");
      Serial.print(curr_pwr/va);
      Serial.print("\t\t");
      
      Serial.print("prev_pwr: ");
      Serial.print(variable_p);
      Serial.print("\n");
      com_count = 0;   
    }
    prev_pwr = curr_pwr;
    digitalWrite(13, LOW);   // reset pin13.
    loopTrigger = 0;
  }
  count += 1;
}


// Timer A CMP1 interrupt. Every 800us the program enters this interrupt. 
// This, clears the incoming interrupt flag and triggers the main loop.

ISR(TCA0_CMP1_vect){
  TCA0.SINGLE.INTFLAGS |= TCA_SINGLE_CMP1_bm; //clear interrupt flag
  loopTrigger = 1;
}

// This subroutine processes all of the analogue samples, creating the required values for the main loop

void sampling(){

  // Make the initial sampling operations for the circuit measurements
  
  sensorValue0 = analogRead(A0); //sample Vb
  sensorValue2 = analogRead(A2); //sample Vref
  sensorValue3 = analogRead(A3); //sample Va
  current_mA = ina219.getCurrent_mA(); // sample the inductor current (via the sensor chip)

  // Process the values so they are a bit more usable/readable
  // The analogRead process gives a value between 0 and 1023 
  // representing a voltage between 0 and the analogue reference which is 4.096V
  
  vb = sensorValue0 * (12400/2400) * (4.096 / 1023.0); // Convert the Vb sensor reading to volts
  vref = sensorValue2 * (4.096 / 1023.0); // Convert the Vref sensor reading to volts
  va = sensorValue3 * (12400/2400) * (4.096 / 1023.0); // Convert the Va sensor reading to volts

  // The inductor current is in mA from the sensor so we need to convert to amps.
  // We want to treat it as an input current in the Boost, so its also inverted
  // For open loop control the duty cycle reference is calculated from the sensor
  // differently from the Vref, this time scaled between zero and 1.
  // The boost duty cycle needs to be saturated with a 0.33 minimum to prevent high output voltages
  
  if (Boost_mode == 1){
    iL = -current_mA/1000.0;
    dutyref = saturation(sensorValue2 * (1.0 / 1023.0),0.99,0.33);
  }else{
    iL = current_mA/1000.0;
    dutyref = sensorValue2 * (1.0 / 1023.0);
  }   
}

float saturation( float sat_input, float uplim, float lowlim){ // Saturatio function
  if (sat_input > uplim) sat_input=uplim;
  else if (sat_input < lowlim ) sat_input=lowlim;
  else;
  return sat_input;
}



void pwm_modulate(float pwm_input){ // PWM function
  analogWrite(6,(int)(255-pwm_input*255)); 
}


/*end of the program.*/

Below is the BUCK smps code for the LED’s:

# Developed by Aymen Jaber and Sakhwat 
from machine import Pin, ADC, PWM

vret_pin = ADC(Pin(26))
vout_pin = ADC(Pin(28))
vin_pin = ADC(Pin(27))
pwm = PWM(Pin(0))
pwm.freq(100000)
pwm_en = Pin(1, Pin.OUT)

count = 0
pwm_out = 0
pwm_ref = 1000
setpoint = 0.0
delta = 100
I_min = 0.075 # this is different for various LED colours
I_max = 0.3 # so does this one

# Define the reference voltage of the ADC (adjust as per your system)
ADC_REFERENCE_VOLTAGE = 3.3

def adc_to_voltage(adc_value): 
    return (adc_value / 65535) * ADC_REFERENCE_VOLTAGE # convert digital reading to voltage

def voltage_to_adc(voltage_value):
    return (voltage_value * 65535) / ADC_REFERENCE_VOLTAGE # convert voltage reading to digital


def saturate(duty): #limit duty cycle to protect LED drivers
    if duty > 62500:
        duty = 62500
    if duty < 100:
        duty = 100
    return duty


def current_limiter(vret, pwm_ref,I_max): # Used to limit current based on I_max
    if vret >= I_max:
        pwm_ref = pwm_ref + delta
    elif vret <= I_min:
        pwm_ref = pwm_ref - delta
    else:
        pwm_ref = pwm_ref
    #return pwm_ref

#PID control variables
# This also varies between LED colours
kpi = 0.5 
kdi = 0
kii = 0.1

def pidi(pid_input)
    e_integration = 0 #initalises e_integration value
    global e1i,e2i #creates global variable that store the previous values 
    e0i = 0.07 - vret
    e_integration = e0i
    u1i = vret
    e2i = e1i
    e1i = pid_input
    
    #anti-windup
    if u1i >= I_max
        e_integration = 0
    elif u1i <= I_min
        e_integration = 0

    delta_ui = kpi * (e0i - e1i) + kii * Ts * e_integration + kdi / Ts * (e0i - 2 * e1i + e2i)
    u0i = u1i + delta_ui
    return u0i 
        
while True:
    pwm_en.value(1)

    vin_bin = vin_pin.read_u16()
    vout_bin = vout_pin.read_u16()
    vret_bin = vret_pin.read_u16()
 
    vin = adc_to_voltage(vin_pin.read_u16())   #This reads the pins in volts rather than binary
    vout = adc_to_voltage(vout_pin.read_u16())
    vret = adc_to_voltage(vret_pin.read_u16())
    count = count + 1
    maxpower = vret * vret
    #I_max = maxpower / vout

    pwm_ref = 25000
    pwm_out = saturate(pwm_ref)
    pwm.duty_u16(pwm_out)
    current_limiter(vret, pwm_out/62500,I_max)
    pwm_out = int(pidi(vret) * 62500)
    
     
for 
    

    if count > 2000:
        print("Vin = {:.0f}".format(vin))
        print("Vout = {:.0f}".format(vout))
        print("Vret = {:.0f}".format(vret))
        print("Duty = {:.0f}".format(pwm_out))
        print("Bin Vin = {:.0f}".format(vin_bin))
        print("Bin Vout = {:.0f}".format(vout_bin))
        print("Bin Vret = {:.0f}".format(vret_bin))
    
        

        
        count = 0

Results

To test the sytem out, I simulated the solar panels using a variable power supply. This is more reliable than using the sun as we can directly control voltage levels.

When the voltage increases, I saw that the boost SMPS was drawing more power. Likewise, when voltage decreases the SMPS draws less power. At the same time, the LED’s worked well when connected to the grid, proving the system to be reliable.

After multiple tests, we saw that the LED’s lasted for a maximum of 20 seconds when voltage is zero.

Below is a video showcasing the system:

And outside with the real panels: