Here is my test code I used to develop a solar tracking system.
/*
Project, use solar cells to generate power
2/2/2020
*/
// https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/esp_timer.html
///esp_timer_get_time(void) //gets time in uSeconds like Arduino Micros. see above link
//////// http://www.iotsharing.com/2017/09/how-to-use-arduino-esp32-can-interface.html
#include "sdkconfig.h"
#include "esp_system.h" //This inclusion configures the peripherals in the ESP system.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
// #include "esp32/ulp.h"
// #include "driver/rtc_io.h"
#include <SimpleKalmanFilter.h>
#include <driver/adc.h>
#include "esp_sleep.h"
#include "driver/mcpwm.h"
const int TaskCore1 = 1;
const int TaskCore0 = 0;
const int TaskStack20K = 20000;
const int Priority3 = 3;
const int Priority4 = 4;
const int SerialDataBits = 115200;
volatile bool EnableTracking = true;
////
//
////
void setup()
{
Serial.begin( 115200 );
// https://dl.espressif.com/doc/esp-idf/latest/api-reference/peripherals/adc.html
// set up A:D channels
log_i( "Setup A:D" );
adc_power_on( );
vTaskDelay( 1 );
adc1_config_width(ADC_WIDTH_12Bit);
// ADC1 channel 0 is GPIO36 (ESP32), GPIO1 (ESP32-S2)
adc1_config_channel_atten(ADC1_CHANNEL_0 , ADC_ATTEN_DB_11); // azimuth 0
// // ADC1_CHANNEL_3 ADC1 channel 3 is GPIO39 (ESP32)
adc1_config_channel_atten(ADC1_CHANNEL_3, ADC_ATTEN_DB_11); // azimuth 1
// // ADC1 channel 5 is GPIO33
adc1_config_channel_atten(ADC1_CHANNEL_5, ADC_ATTEN_DB_11); // altitude 1
// // ADC1 channel 6 is GPIO34
adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11); // altitude 0
// // adc for light dark detection, ADC1 channel 7 is GPIO35 (ESP32)
adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_11);
// adc for solar cell voltage detection, ADC1 channel 4 is GPIO33 (ESP32)
adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11);
//
log_i( "A:D setup complete" );
// control for relay to supply power to the servos
pinMode( 5, OUTPUT );
vTaskDelay(1);
log_i( "setup MCPWM" );
//
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_NUM_4 ); // Azimuth
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_NUM_12 ); // Altitude servo
//
mcpwm_config_t pwm_config = {};
pwm_config.frequency = 50; //frequency = 50Hz, i.e. for every servo motor time period should be 20ms
pwm_config.cmpr_a = 0; //duty cycle of PWMxA = 0
pwm_config.cmpr_b = 0; //duty cycle of PWMxb = 0
pwm_config.counter_mode = MCPWM_UP_COUNTER;
pwm_config.duty_mode = MCPWM_DUTY_MODE_0;
log_i( "MCPWM complete" );
//
//
mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config); //Configure PWM0A timer 0 with above settings
mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_1, &pwm_config); //Configure PWM0A timer 1 with above settings
// ////
fMoveAltitude( 1525 );
fMoveAzimuth( 1500 );
////
xTaskCreatePinnedToCore( TrackSun, "TrackSun", TaskStack20K, NULL, Priority3, NULL, TaskCore1 ); // assigned to core
xTaskCreatePinnedToCore( fDaylight, "fDaylight", TaskStack20K, NULL, Priority3, NULL, TaskCore0 ); // assigned to core
//// // esp_sleep_enable_timer_wakeup( (60000000 * 2) ); // set timer to wake up once a minute 60000000uS * 2
}
//
void fDaylight( void * pvParameters )
{
int lightLevel = 0;
log_i( "Start up fDaylight" );
while (1)
{
// sufficent voltage at the solar cells to do the thing?
log_i( "Solar Cell: %d", adc1_get_raw(ADC1_CHANNEL_4) );
if (adc1_get_raw(ADC1_CHANNEL_4) > 1600 )
{
lightLevel = adc1_get_raw(ADC1_CHANNEL_7);
if ( adc1_get_raw(ADC1_CHANNEL_7) >= 300 )
{
log_i( "Light Level: %d", lightLevel );
EnableTracking = true;
digitalWrite( 5, LOW ); // power to relay module/servos
vTaskDelay( 1 );
} else {
// if light level to low park boom
fMoveAltitude( 1475 );
fMoveAzimuth( 1900 );
// put esp32 into deep sleep
digitalWrite( 5, HIGH ); // deenergize servo motors
EnableTracking = false;
esp_sleep_enable_timer_wakeup( (60000000 * 5) ); // set timer to wake up once a minute 60000000uS
esp_deep_sleep_start();
}
} else {
digitalWrite( 5, HIGH ); // deenergize servo motors
EnableTracking = false;
esp_sleep_enable_timer_wakeup( (60000000 * 5) ); // set timer to wake up once a minute 60000000uS
esp_deep_sleep_start();
}
vTaskDelay( 1000 );
} // while(1)
} // void fDaylight( void * pvParameters )
////
/**
@brief Use this function to calcute pulse width for per degree rotation
@param degree_of_rotation the angle in degree to which servo has to rotate
@return
- calculated pulse width
*/
//static uint32_t servo_per_degree_init(uint32_t degree_of_rotation)
//{
// const int SERVO_MIN_PULSEWIDTH = 500; //Minimum pulse width in microsecond
//const int SERVO_MAX_PULSEWIDTH 2500 //Maximum pulse width in microsecond
//const int SERVO_MAX_DEGREE 180 //Maximum angle in degree upto which servo can rotate
// uint32_t cal_pulsewidth = 0;
// cal_pulsewidth = (SERVO_MIN_PULSEWIDTH + (((SERVO_MAX_PULSEWIDTH - SERVO_MIN_PULSEWIDTH) * (degree_of_rotation)) / (SERVO_MAX_DEGREE)));
// return cal_pulsewidth;
//}
////
void mcpwm_gpio_initialize(void)
{
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM1A, GPIO_NUM_4 );
mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, GPIO_NUM_12 );
}
////
// void TrackSun( int Altitude, int Azimuth )
void TrackSun( void * pvParameters )
{
log_i( "Startup TrackSun" );
int64_t EndTime = esp_timer_get_time();
int64_t StartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros
int Altitude = 1500;
int Azimuth = 900;
int maxAltitudeRange = 2144;
int minAltitudeRange = 900;
int maxAzimuthRange = 2144;
int minAzimuthRange = 900;
float AltitudeThreashold = 10.0f;
float AzimuthThreashold = 10.0f;
float kalmanThreshold = 80.0f;
SimpleKalmanFilter kfAltitude0( AltitudeThreshold, kalmanThreshold, .01 ); // kalman filter Altitude 0
SimpleKalmanFilter kfAltitude1( AltitudeThreshold, kalmanThreshold, .01 ); // kalman filter Altitude 1
SimpleKalmanFilter kfAzimuth0( AzimuthThreshold, kalmanThreshold, .01 ); // kalman filter Azimuth 0
SimpleKalmanFilter kfAzimuth1( AzimuthThreshold, kalmanThreshold, .01 ); // kalman filter Azimuth 1
float filteredAltitude_0 = 0.0f;
float filteredAltitude_1 = 0.0f;
float filteredAzimuth_0 = 0.0f;
float filteredAzimuth_1 = 0.0f;
uint64_t AzimuthEndTime = esp_timer_get_time();
uint64_t AzimuthStartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros,but rolls over after 207 years
uint64_t AltitudeEndTime = esp_timer_get_time();
uint64_t AltitudeStartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros,
int StartupCount = 0;
int TorqueAmmount = 5;
while (1)
{
if ( EnableTracking )
{
//Altitude
AltitudeEndTime = esp_timer_get_time() - AltitudeStartTime; // produce elapsed time for the simpleKalmanFilter
kfAltitude0.setProcessNoise( float(AltitudeEndTime) / 1000000.0f );
kfAltitude1.setProcessNoise( float(AltitudeEndTime) / 1000000.0f );
// Serial.println( AltitudeEndTime,6 );
filteredAltitude_0 = kfAltitude0.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_6) );
filteredAltitude_1 = kfAltitude1.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_5) );
// Serial.print( filteredAltitude_0 );
// Serial.print( ", " );
// Serial.print( filteredAltitude_1 );
// Serial.print( ", " );
// Serial.println();
if ( (filteredAltitude_0 > filteredAltitude_1) && (abs(filteredAltitude_0 - filteredAltitude_1) > AltitudeThreshold))
{
Altitude -= TorqueAmmount;
// log_i( "> Alt %d", Altitude );
if ( Altitude < minAltitudeRange )
{
Altitude = 900;
}
fMoveAltitude( Altitude );
AltitudeStartTime = esp_timer_get_time();
}
if ( (filteredAltitude_0 < filteredAltitude_1) && (abs(filteredAltitude_0 - filteredAltitude_1) > AltitudeThreshold) )
{
Altitude += TorqueAmmount;
if ( Altitude >= maxAltitudeRange )
{
Altitude = 900;
}
fMoveAltitude( Altitude );
// log_i( "< Alt %d", Altitude );
AltitudeStartTime = esp_timer_get_time();
}
//// AZIMUTH
AzimuthEndTime = esp_timer_get_time() - AzimuthStartTime; // produce elasped time for the simpleKalmanFilter
// Serial.print( (float)adc1_get_raw(ADC1_CHANNEL_3) );
// Serial.print( "," );
// Serial.print( (float)adc1_get_raw(ADC1_CHANNEL_0) );
// Serial.print( "," );
kfAzimuth0.setProcessNoise( float(AzimuthEndTime) / 1000000.0f ); //convert time of process to uS, update SimpleKalmanFilter q
kfAzimuth1.setProcessNoise( float(AzimuthEndTime) / 1000000.0f ); //convert time of process to uS, update SimpleKalmanFilter q
filteredAzimuth_0 = kfAzimuth0.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_3) );
filteredAzimuth_1 = kfAzimuth1.updateEstimate( (float)adc1_get_raw(ADC1_CHANNEL_0) );
// Serial.print( filteredAzimuth_0 );
// Serial.print( ", " );
// Serial.print( filteredAzimuth_1 );
// Serial.println();
if ( (filteredAzimuth_0 > filteredAzimuth_1) && (abs(filteredAzimuth_0 - filteredAzimuth_1)) > AzimuthThreashold )
{
Azimuth += TorqueAmmount;
if ( Azimuth >= maxAzimuthRange )
{
Azimuth = 900;
}
// log_i( "> Az %d", Azimuth );
fMoveAzimuth( Azimuth );
AzimuthStartTime = esp_timer_get_time();
}
// //
if ( (filteredAzimuth_0 < filteredAzimuth_1) && (abs(filteredAzimuth_1 - filteredAzimuth_0)) > AzimuthThreashold )
{
// log_i( "< Az %d", Azimuth );
Azimuth -= TorqueAmmount; // make new position to torque to
if ( (Azimuth >= maxAzimuthRange) || (Azimuth <= minAzimuthRange) )
{
Azimuth = 900;
}
fMoveAzimuth( Azimuth );
AzimuthStartTime = esp_timer_get_time();
} //azmiuth end
if ( StartupCount < 500 )
{
++StartupCount;
} else {
TorqueAmmount = 1;
// esp_sleep_enable_timer_wakeup( (60000000 / 30) ); // set timer to wake up
// esp_sleep_enable_timer_wakeup( (60000000 / 20) ); // set timer to wake up
// esp_light_sleep_start();
}
}
vTaskDelay( 65 );
} // while(1)
} //void TrackSun()
////
void fMoveAltitude( int MoveTo )
{
mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, MoveTo);
vTaskDelay( 12 );
}
//////
void fMoveAzimuth( int MoveTo )
{
mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_1, MCPWM_OPR_A, MoveTo);
vTaskDelay( 12 );
}
////
void loop() {}
////
You'll have to translate it to work with a non-ESP32 MCU but the basic principle is there. Look at the task TrackSun()
.
I used photodiodes set at each end and slightly below the surface of the solar array. I found photodiodes work way better than LDR's.
In the end I found that using a solar tracking library that calculates the suns location given a position and torquing the solar cells once every 6 minutes to a new position was more energy saving then a continuous tracker. I only apply power to the motors when they need torquing instead of the motors being a continuous power draw.