Satnogs rotator v 3 firmware meridian flip fixed - 2026

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.

:bar_chart: 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

:white_check_mark: MAJOR IMPROVEMENT: Correct and intelligent Meridian Flip

This is the most important change and solves your original problem.

:red_circle: 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.

:green_circle: 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);
:check_mark: 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°
:check_mark: 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.

:white_check_mark: 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

:white_check_mark: 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.

:check_mark: Advantages

No sudden jumps

Smooth movement

No loss of synchronization

:white_check_mark: 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

:white_check_mark: 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
:white_check_mark: 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

:white_check_mark: IMPROVEMENT 7: Precise step ↔ degree conversions

Maintains your real ratio:

#define SPR 200
#define RATIO 54

So perfect for your NEMA17

REGARD - IZ5RZR

Merifian flip.txt (6.4 KB)

1 Like

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.

:bar_chart: 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

:white_check_mark: MAJOR IMPROVEMENT: Correct and intelligent Meridian Flip

This is the most important change and solves your original problem.

:red_circle: 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.

:green_circle: 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);
:check_mark: 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°
:check_mark: 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.

:white_check_mark: 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

:white_check_mark: 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.

:check_mark: Advantages

No sudden jumps

Smooth movement

No loss of synchronization

:white_check_mark: 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

:white_check_mark: 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
:white_check_mark: 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

:white_check_mark: 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)

2 Likes

Nice, thanks! Just in time as I resurrecting my rotator project. I will check it out, probably a little mod on the pin wiring so it can run on Arduino Uno

1 Like