Pan&Tilt with Stepper motors for heliostat project

Hello,

I am trying to build an heliostat using a mirror and 2 stepper motors in a Pan&tilt configuration.
Basically, the intention is to keep the mirror in a calculated position to reflect the sunlight always to the same spot throughout the day.

I'll use an Arduino UNO R3 board, a DS3231 real time clock, and 2 stepper motors 28BYJ-48 with 2 driver ULN2003.
For power input, I'll use 5V from the MB102 breadboard since I read that it's not good to power the steppers directly from the UNO board.

My code compiles and the calculations to determine angle instructions seem to work well. (I verified all variables calculated via the serial monitor)
I have not tested yet the steppers instructions as I don't have in hand the second stepper and driver yet, and I would also like to be sure of my code before trying it out in order not to damage anything...

I'm a newbie and it's my first time trying to build a code, so I'm pretty sure there is a lot to optimize, and probably a lot to correct as well. I did my best with the knowledge I could...

I'd very much appreciate your input to tell me whether this code can work and if it could be improved somehow.
I'm not looking for something perfect, but at least to have something viable.
I'm also interested if you have any advice on the hardware that I intend to use.

A few more specific questions I have:

  • I added some enable/disable outputs function on the stepper, with in mind the feeling that it was useless to keep current going if the motor is not moving anyway. Does it make sense ?
  • Based on another code I took example from, I also included a function to check whether the previous movement is over before starting next one. Not sure if that's necessary actually since the code should not continue to next instruction unless previous one if finished, right ?
  • Are the stepper speeds I'm considering ok ? With the Accelstepper library, normally the speeds are in steps per seconds, so should be ok, right ?
  • About the 28BYJ-48 steppers, I see in the datasheet 2048 full steps, but I also saw as per calculation that we actually have 2038 steps precisely. Any idea why do we have this discrepancy ?

Thanks for your support !


#include <AccelStepper.h>
#include <MultiStepper.h>
#include <RTClib.h>
#include <Wire.h>
RTC_DS3231 rtc;

const float Latitude = 43.32138, Longitude = 5.44302;  //Position of the heliostat (random in this example).
float LatRad, LongRad;
float TempsUTC;
const int Angleface = -80, Anglefocal = 15;
float AngFaceRad, AngFocalRad;
int Daynumb;
float Declinaison, DecliRad, B, EquTemps, AngHoraire, HauteurSol, AzimutSol;
float AngleIncidence, InclinaisonMiroir, cosphiae, PhiAE ;

const float stepresolution = 360 / 2038 ; // Resolution of the 2 stepper motors 28BYJ-48.
float DegreeAz_order ;
float DegreeH_order ;
float StepAz_order = DegreeAz_order / stepresolution ;
float StepH_order = DegreeH_order / stepresolution ;
bool move_finishedAz ; // To check that previous movement is ended prior starting next one.
bool move_finishedH ;  // To check that previous movement is ended prior starting next one.

//For mirror Azimut motor:
#define motorAzPin1 8 // IN1 sur le pilote ULN2003
#define motorAzPin2 9 // IN2 sur le pilote ULN2003
#define motorAzPin3 10 // IN3 sur le pilote ULN2003
#define motorAzPin4 11 // IN4 sur le pilote ULN2003
#define MotorAzInterfaceType 4
AccelStepper stepperAz = AccelStepper ( MotorAzInterfaceType, motorAzPin1, motorAzPin3, motorAzPin2, motorAzPin4 ) ;

//For mirror Height motor:
#define motorHPin1 4 // IN1 sur le pilote ULN2003
#define motorHPin2 5 // IN2 sur le pilote ULN2003
#define motorHPin3 6 // IN3 sur le pilote ULN2003
#define motorHPin4 7 // IN4 sur le pilote ULN2003
#define MotorHInterfaceType 4
AccelStepper stepperH = AccelStepper ( MotorHInterfaceType, motorHPin1, motorHPin3, motorHPin2, motorHPin4 ) ;


void  setup()
{
  Serial.begin(9600);
  LatRad = radians(Latitude);
  LongRad = radians(Longitude);
  AngFaceRad = radians(Angleface);
  AngFocalRad = radians(Anglefocal);

  rtc.begin();

  stepperAz.setCurrentPosition(0);      //Upon start, mirror must be facing South.
  stepperAz.setMaxSpeed ( 500 ) ;
  stepperAz.setAcceleration(200);
  move_finishedAz = 1 ;
  stepperAz.disableOutputs();           //Turn power off on the motor by default.
  stepperH.setCurrentPosition(0);       //Upon start, mirror must be vertical.
  stepperH.setMaxSpeed ( 300 ) ;
  stepperH.setAcceleration(100);
  move_finishedH = 1 ;
  stepperH.disableOutputs();            //Turn power off on the motor by default.
}

void loop()
{
  DateTime now = rtc.now();
  while (now.hour() >= 8 && now.hour() <= 20 ) {
    //Define daily time range of operation. From 08h00 till 20h00 (local time) in this case.

    Daynumb = (now.unixtime() % 31556926) / 86400;
    TempsUTC = -1 + float(now.hour()) + float(now.minute()) / 60 + float(now.second()) / 3600 ;
    //Determine day and time to be used in astronomical calculations. Including time correction for UTC (-1h in this case).

    //Start of ASTRONOMICAL CALCULATIONS ****************************************************************************************************
        Declinaison = (180 / PI) * asin(sin(PI * 23.44 / 180) * sin((2 * PI / 365.25) * (Daynumb - 81)));
        DecliRad = radians(Declinaison);
        //Declinaison of the Sun based on day of the year since 01st of January.
    
        B = 2 * PI * (Daynumb - 81) / 365.25;
        EquTemps = 7.53 * cos(B) + 1.5 * sin(B) - 9.87 * sin(2 * B);
        AngHoraire = ((TempsUTC - 12) + (Longitude / 15) - EquTemps / 60) * PI / 12;
        //Time angle of the Sun based on Longitude and exact time.
    
        HauteurSol = asin(sin(LatRad) * sin(DecliRad) + cos(LatRad) * cos(DecliRad) * cos(AngHoraire));
        //Height of the sun versus horizon, in radians.
        AzimutSol = asin(cos(DecliRad) * sin(AngHoraire) / cos(HauteurSol));
        //Sun's Azimut from South in radians. Counted negative to East.
    
        AngleIncidence = 0.5 * acos(sin(AngFocalRad) * sin(HauteurSol)
                         + cos(AngFocalRad) * sin(AngFaceRad) * cos(HauteurSol) * sin(AzimutSol)
                         + cos(AngFocalRad) * cos(AngFaceRad) * cos(HauteurSol) * cos(AzimutSol));
    
        DegreeH_order = degrees(asin((sin(AngFocalRad) + sin(HauteurSol)) / (2 * cos(AngleIncidence))));
        // Mirror tilt in degrees. Vertical being 0degrees.
    
        cosphiae = (cos(HauteurSol) * cos(AzimutSol) + cos(AngFocalRad) * cos(AngFaceRad)) / (2 * cos(AngleIncidence) * cos(InclinaisonMiroir));
    
        if (cosphiae >= 0) {
          PhiAE = asin((cos(HauteurSol) * sin(AzimutSol) + cos(AngFocalRad) * sin(AngFaceRad)) / (2 * cos(AngleIncidence) * cos(InclinaisonMiroir)));
        }
        else {
          PhiAE = PI - asin((cos(HauteurSol) * sin(AzimutSol) + cos(AngFocalRad) * sin(AngFaceRad)) / (2 * cos(AngleIncidence) * cos(InclinaisonMiroir)));
        }
        DegreeAz_order = degrees(PhiAE);
        //Mirror Azimut in degrees. South being 0degrees, East negative, West positive.
    //End of ASTRONOMICAL CALCULATIONS****************************************************************************************************

    //Instructions for mirror's Azimutal and Height motors.

    stepperAz.moveTo(StepAz_order);
    if (move_finishedAz = 1 && (stepperAz.distanceToGo() != 0)) { //Check that motor not currently moving, and order is different then current position.
      move_finishedAz = 0 ;
      stepperAz.enableOutputs();
      stepperAz.run();
    }
    if (stepperAz.distanceToGo() == 0) {
      move_finishedAz = 1 ;
      stepperAz.disableOutputs();
    }

    stepperH.moveTo(StepH_order);
    if (move_finishedH = 1 && (stepperH.distanceToGo() != 0)) {
      move_finishedH = 0 ;
      stepperH.enableOutputs();
      stepperH.run();
    }
    if (stepperH.distanceToGo() == 0) {
      move_finishedH = 1 ;
      stepperH.disableOutputs();
    }

    delay(10000); //Frequency of position adjustements.
  }
}

Those steppers are mass produced for duct/vent control, and the gear ratios can vary. Some of them do not lead to a precise number of steps per revolution. Here is one example, but you have to check your particular motor to see if it conforms: In-Depth: Control 28BYJ-48 Stepper Motor with ULN2003 Driver & Arduino

Example of a pan-tilt tracker and heliostat, using a 28BYJ for the azimuth: GitHub - jremington/Arduino_heliostat: Model alt/az heliostat using Arduino

I'm surprised if this works, as the result of integer division is zero. If it does, the compiler is smart enough to understand that a floating point constant is required here:

const float stepresolution = 360 / 2038 ; // Resolution of the 2 stepper motors 28BYJ-48.

It seems unnecessary to be doing the sun position calculations continuously. I'd be more inclined to do it no more often than once a minute. It would give the steppers time to finish their moves too.

@jremington , thanks a lot for the link to the github project ! I'm surprised I could not find it earlier...
I'll have a good look into it to see if I can understand the code and apply it on my side.
I had already seen the page on lastminuteengineers, which was indeed a big help to understand the motor and how to code it.
About the stepresolution, you mean that to make it "clean" I would need to define 360 and 2038 as float values as well ?

const float stepresolution = float 360 / float 2038 ;

@wildbill , indeed, it's set for every 10 seconds in this first code, but I'll most probably fine tune to calculate the sun position only every minute or so. It's the role of the delay function at the very end of my code.

As next step, I have tried some tests with the steppers, but for some reason, using the run() function doesn't seem to work... It only works when I use the runToPosition() instead, although the Accelstepper documentation says that this function should not be used the loop() part since it blocks.
Could you help to clarify what's the difference between the 2 ? (only the description from documentation isn't really making it clearer to me...)

In below simple example, motor only moves by 1 single step every 3 seconds, but if I replace run by runToPosition, it works... Why is that ?

#include <AccelStepper.h>
#define motorPin1 8 // IN1 sur le pilote ULN2003
#define motorPin2 9 // IN2 sur le pilote ULN2003
#define motorPin3 10 // IN3 sur le pilote ULN2003
#define motorPin4 11 // IN4 sur le pilote ULN2003
#define HALFSTEP 8
AccelStepper stepperX = AccelStepper( HALFSTEP, motorPin1, motorPin3, motorPin2, motorPin4) ;

int StepAzorder;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  stepperAz.setMaxSpeed(500) ;
  stepperAz.setAcceleration(200);
  stepperAz.setSpeed(300);

}

void loop() {
  // put your main code here, to run repeatedly:

StepAzorder = 4096;
        stepperAz.moveTo(StepAzorder);
        stepperAz.run();
delay(3000);

StepAzorder = 0;
        stepperAz.moveTo(StepAzorder);
        stepperAz.run();
delay(3000);
}

I missed the delay for timing - oops. However, it's the cause of your problem. Run causes a single step if it's time for one, so you have to call it frequently. Delay interferes with that. RunToPosition apparently does all steps required but it blocks while doing so which is usually a problem if you want the system to do other things like respond to buttons. In your case, since you're delaying anyway, it doesn't matter (yet).

This is what most people would write:
const float stepresolution = 360.0 / 2038.0 ;

But depending on the actual gear ratio, there is no reason to throw away accuracy:
const float stepresolution = 360.0 / 2037.89 ;

@jremington , well noted about the decimals, I'll use that.

@wildbill Thanks for your help about the delay, it led me in the right direction to find my solution. After some additional research online, I managed to make it work using the millis() function instead to calculate my variables at a chosen frequency.
I have to admit it took me some time to understand the actual meaning of "calling run() frequently"...

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.