Hello.
Years ago, I published the firmware to be loaded onto the Leonardo Pro-Micro for controlling the Satellite V2 and V3 stepper motor rotors. This firmware was implemented with the resolution of the meridian flip issue. I’ve used it so far in my three rotors, and it’s worked correctly for reception in the VHF, UHF, L-band, S-band, and recently, X-band.
However, I decided to see if I could further improve performance, so I’ve reworked the code to make it work even better.
Final summary of improvements
Improvement Result
Intelligent meridian flip eliminates unnecessary full rotations
Normalization of total stability angles
Relative movement, no jumps or errors
Stable zero homing correct
EasyComm compatibility perfect with SatNOGS
Automatic shutdown for safety and durability
Precise conversions compatible with 54:1
MAJOR IMPROVEMENT: Correct and intelligent Meridian Flip
This is the most important change and solves your original problem.
Original firmware issue
When the azimuth changed from:
359° → 0°
the rotor did this:
359 → 0 passing through 358, 357, 356 … (full counterclockwise rotation)
that is, it made almost a full rotation → risk of cable twisting.
Implemented solution
This section of the code is the key:
double currentAz = step2deg(AZstepper.currentPosition());
double targetAz = angleAz;
currentAz = fmod(currentAz, 360.0);
if (currentAz < 0) currentAz += 360.0;
targetAz = fmod(targetAz, 360.0);
if (targetAz < 0) targetAz += 360.0;
double delta = targetAz - currentAz;
delta = fmod(delta + 540.0, 360.0) - 180.0;
stepAz = AZstepper.currentPosition() + deg2step(delta);
What exactly does it do?
Calculates the shortest path instead of always going to absolute 0.
Example:
Current Target Position Resulting Movement
350° 10° +20° (CORRECT)
10° 350° −20° (CORRECT)
180° 190° +10°
180° 170° −10°
Result
The rotor:
never makes unnecessary full revolutions
does not wrap the cables
always follows the shortest path
This is the professional behavior of commercial rotors.
IMPROVEMENT 2: Normalization of 0-360° angles
This part:
currentAz = fmod(currentAz, 360.0);
if (currentAz < 0) currentAz += 360.0;
Ensures that the angles are always:
0° → 359.999°
Avoid errors when:
The motor exceeds 360°
The motor goes negative
Tracking lasts for many orbits
IMPROVEMENT 3: Maintains the motor’s real absolute position
This is very important:
stepAz = AZstepper.currentPosition() + deg2step(delta);
DOES NOT do:
stepAz = deg2step(targetAz);
but adds the delta to the actual position.
Advantages
No sudden jumps
Smooth movement
No loss of synchronization
IMPROVEMENT 4: Perfect compatibility with EasyComm 2
These commands work perfectly:
AZxxx.x ELxxx.x
AZ EL (read position)
RESET
SA SE
Compatible with:
SatNOGS
GPredict
Orbitron
rotctl
IMPROVEMENT 5: Stable and safe homing
The system:
Homing(deg2step(-ANGLE_SCANNING_MULT), deg2step(-ANGLE_SCANNING_MULT));
automatically finds the limit switch.
and then:
AZstepper.setCurrentPosition(0);
ELstepper.setCurrentPosition(0);
Set:
AZ = 0° real
EL = 0° real
IMPROVEMENT 6: Automatic motor disable
This part:
if (!AZstepper.isRunning() && !ELstepper.isRunning() && millis() - t_DIS > T_DELAY)
{
digitalWrite(EN, HIGH);
}
Turns off the driver after inactivity.
Benefits:
Less heat
Less power consumption
Longer driver life
IMPROVEMENT 7: Precise step ↔ degree conversions
Maintains your real ratio:
#define SPR 200
#define RATIO 54
So perfect for your NEMA17
REGARD - IZ5RZR
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <AccelStepper.h>
#define DIR_AZ 18
#define STEP_AZ 10
#define DIR_EL 6
#define STEP_EL 7
#define MS1 9
#define EN 8
#define SPR 200
#define RATIO 54
#define T_DELAY 60000
#define HOME_AZ 4
#define HOME_EL 5
#define ANGLE_SCANNING_MULT 180
#define MAX_AZ_ANGLE 360
#define MAX_EL_ANGLE 360
#define HOME_DELAY 6000
unsigned long t_DIS = 0;
AccelStepper AZstepper(1, STEP_AZ, DIR_AZ);
AccelStepper ELstepper(1, STEP_EL, DIR_EL);
void setup()
{
AZstepper.setMaxSpeed(150);
AZstepper.setAcceleration(50);
ELstepper.setMaxSpeed(150);
ELstepper.setAcceleration(50);
pinMode(EN, OUTPUT);
digitalWrite(EN, LOW);
pinMode(MS1, OUTPUT);
digitalWrite(MS1, LOW);
pinMode(HOME_AZ, INPUT);
pinMode(HOME_EL, INPUT);
Serial.begin(19200);
Homing(deg2step(-ANGLE_SCANNING_MULT), deg2step(-ANGLE_SCANNING_MULT));
}
void loop()
{
static long AZstep = 0;
static long ELstep = 0;
if (t_DIS == 0)
t_DIS = millis();
if (!AZstepper.isRunning() && !ELstepper.isRunning() && millis() - t_DIS > T_DELAY)
{
digitalWrite(EN, HIGH);
}
else
{
digitalWrite(EN, LOW);
}
cmd_proc(AZstep, ELstep);
stepper_move(AZstep, ELstep);
}
/* ================= HOMING (INALTERATO) ================= */
void Homing(long AZsteps, long ELsteps)
{
int value_Home_AZ = HIGH;
int value_Home_EL = HIGH;
int n_AZ = 1;
int n_EL = 1;
boolean isHome_AZ = false;
boolean isHome_EL = false;
AZstepper.moveTo(AZsteps);
ELstepper.moveTo(ELsteps);
while (isHome_AZ == false || isHome_EL == false)
{
value_Home_AZ = digitalRead(HOME_AZ);
value_Home_EL = digitalRead(HOME_EL);
if (value_Home_AZ == HIGH)
{
AZstepper.moveTo(AZstepper.currentPosition());
isHome_AZ = true;
}
if (value_Home_EL == HIGH)
{
ELstepper.moveTo(ELstepper.currentPosition());
isHome_EL = true;
}
if (AZstepper.distanceToGo() == 0 && !isHome_AZ)
{
n_AZ++;
AZsteps = deg2step(pow(-1, n_AZ) * n_AZ * ANGLE_SCANNING_MULT);
if (abs(n_AZ * ANGLE_SCANNING_MULT) > MAX_AZ_ANGLE)
{
error(0);
break;
}
AZstepper.moveTo(AZsteps);
}
if (ELstepper.distanceToGo() == 0 && !isHome_EL)
{
n_EL++;
ELsteps = deg2step(pow(-1, n_EL) * n_EL * ANGLE_SCANNING_MULT);
if (abs(n_EL * ANGLE_SCANNING_MULT) > MAX_EL_ANGLE)
{
error(1);
break;
}
ELstepper.moveTo(ELsteps);
}
AZstepper.run();
ELstepper.run();
}
long time = millis();
while (millis() - time < HOME_DELAY)
{
AZstepper.run();
ELstepper.run();
}
AZstepper.setCurrentPosition(0);
ELstepper.setCurrentPosition(0);
}
/* ================= EASYCOMM 2 ================= */
void cmd_proc(long &stepAz, long &stepEl)
{
char buffer[256];
char incomingByte;
static int counter = 0;
char *p;
char *str;
char data[100];
double angleAz, angleEl;
while (Serial.available() > 0)
{
incomingByte = Serial.read();
if (incomingByte == '!')
{
Serial.print("TM1 AZ");
Serial.print(10 * step2deg(AZstepper.currentPosition()), 1);
Serial.print(" EL");
Serial.println(10 * step2deg(ELstepper.currentPosition()), 1);
}
else if (incomingByte == '\n')
{
buffer[counter] = 0;
p = buffer;
if (buffer[0] == 'A' && buffer[1] == 'Z')
{
if (buffer[2] == ' ' && buffer[3] == 'E' && buffer[4] == 'L')
{
Serial.print("AZ");
Serial.print(step2deg(AZstepper.currentPosition()), 1);
Serial.print(" EL");
Serial.println(step2deg(ELstepper.currentPosition()), 1);
}
else
{
str = strtok_r(p, " ", &p);
strncpy(data, str + 2, 10);
angleAz = atof(data);
/* ===== Improved Meridian Flip ===== */
double currentAz = step2deg(AZstepper.currentPosition());
double targetAz = angleAz;
currentAz = fmod(currentAz, 360.0);
if (currentAz < 0) currentAz += 360.0;
targetAz = fmod(targetAz, 360.0);
if (targetAz < 0) targetAz += 360.0;
double delta = targetAz - currentAz;
delta = fmod(delta + 540.0, 360.0) - 180.0;
stepAz = AZstepper.currentPosition() + deg2step(delta);
/* ===== Elevation ===== */
str = strtok_r(p, " ", &p);
if (str[0] == 'E' && str[1] == 'L')
{
strncpy(data, str + 2, 10);
angleEl = atof(data);
stepEl = deg2step(angleEl);
}
}
}
else if (strncmp(buffer, "SA SE", 5) == 0)
{
Serial.print("AZ");
Serial.print(step2deg(AZstepper.currentPosition()), 1);
Serial.print(" EL");
Serial.println(step2deg(ELstepper.currentPosition()), 1);
stepAz = AZstepper.currentPosition();
stepEl = ELstepper.currentPosition();
}
else if (strncmp(buffer, "RESET", 5) == 0)
{
Serial.print("AZ");
Serial.print(step2deg(AZstepper.currentPosition()), 1);
Serial.print(" EL");
Serial.println(step2deg(ELstepper.currentPosition()), 1);
Homing(0, 0);
stepAz = 0;
stepEl = 0;
}
counter = 0;
t_DIS = 0;
}
else
{
if (counter < sizeof(buffer) - 1)
{
buffer[counter++] = incomingByte;
}
}
}
}
/* ================= ERROR ================= */
void error(int num_error)
{
switch (num_error)
{
case 0:
while (1)
{
Serial.println("AL001");
delay(100);
}
case 1:
while (1)
{
Serial.println("AL002");
delay(100);
}
default:
while (1)
{
Serial.println("AL000");
delay(100);
}
}
}
/* ================= STEPPER ================= */
void stepper_move(long stepAz, long stepEl)
{
AZstepper.moveTo(stepAz);
ELstepper.moveTo(stepEl);
AZstepper.run();
ELstepper.run();
}
/* ================= CONVERSIONI ================= */
long deg2step(double deg)
{
return (long)(RATIO * SPR * deg / 360.0);
}
double step2deg(long Step)
{
return 360.0 * (double)Step / (SPR * RATIO);
}
Merifian flip.txt (6.4 KB)