Professional Antenna Rotator Controller – Seamless Integration with Tracking Software
Components and supplies
![]() |
| × | 1 | |||
![]() |
| × | 2 | |||
![]() |
| × | 2 | |||
![]() |
| × | 1 | |||
| × | 2 | ||||
![]() |
| × | 2 |
Apps and online services
![]() |
|
About this project
Latest update November 2021
This project started as an entertainment and became a serious piece of equipment.
The Controller accepts manual positioning of the antenna, by means of two rotary encoders, Azimuth and Elevation. It can automatically track satellites, when connected by USB to a PC running satellite tracking software.
It's compatible with all tracking software using EasyComm2 protocol / 9600 bauds. PstRotator, WXtrack, HRD, MacDoppler... Even WXtoIMG can control the rotator.
It works directly with Orbitron, with a DDE plugin from http://tripsintech.com/orbitron-dde-azimuth-elevation-to-serial/
The controller outputs a response on serial, for the tracking software to display the real antenna position on the screen. So far, only PstRotator did that.
The code doesn't use any library (except for the LCD) and runs exactly as is, with pins according to the electric diagram below. If you press the button of the Azimuth encoder, all antenna movement stops immediately, and the azimuth command can be set in 10deg. steps.
You will find here two versions: One for DC motors, and one for AC motors (just relays). The later one can be interfaced with existing commercial antenna rotators.
The DC motors version has the advantage of using PWM for a softer/smoother antenna movement. It outputs a power response proportional with angle error (Target<->Antenna). Therefore, when the antenna starts moving, it accelerates progressively, and, when approaching the desired position, it slows down until full stop. This is known as Soft-Start / Soft-Stop. There's an adjustable Dead Zone, where the antenna doesn't move for the slightest target offset.

I have a beta version with Soft-Start / Soft-Stop for AC motors, taking advantage of this AC-Dimmer module, but right now it only works for azimuth. If you want to give it a try, let me know by email.
If you have 180deg. elevation system, you're good, give me an email. There's also a version with 0.1deg. precision, but I wouldn't recommend it unless you have a goddamn rock solid potentiometer reading and a paranoiac controller construction design. You'll find more versions on my web page.
After finishing the construction, you must apply the calibration procedures.
- The potentiometer calibration is mandatory and ensures correct reading of 0-359deg. / 0-90deg., no matter what kind of potentiometer you're using.
- The motor calibration is only for tuning the Soft-Start-Stop feature. This is necessary if you don't like the default settings.
More detailed explanations in the videos. Because the code have been improved over time, and the videos can't be updated anymore, check my web-page for the latest information and personal experience living with this controller. https://racov.ro/index.php/2020/12/09/arduino-based-antenna-rotator-part3-software-tracking-update/
Give me an email if you want to know more, because this platform doesn't inform me of new comments, dunno why. I'll try to solve small problems the best I can. YO3RAK@gmail.com
Thank you very much to all who sent me feedback, helping to make this project more reliable. Any feedback is highly appreciated.
Code
- ant-rot-DC-nov2021
- ant-rot-AC-aug2021
- Potentiometer calibration procedure
- Motor calibration procedure
ant-rot-DC-nov2021Arduino
This code is for DC motors, with soft start-stop PWM output/* AZ/EL Antenna Rotator controller for Arduino - DC motors
* ========================================================
* Uses EasyComm protocol for computer - Tracking Software
* Manual command by means of two rotary encoders AZ - EL
*
* Viorel Racoviteannu
* https://www.youtube.com/channel/UCiRLZX0bV9rS04BGAyUf-fA
* https://racov.ro
* YO3RAK@gmail.com
*
* I cannot take any responsibility for missuse of this code
* or any kind of damage it may occur from using this code.
*
* dec 2020 v2 - improved serial comm stability
* jan 2021 - improved near target dead-zone, for which antenna won't move
* apr 2021 - improved serial comm stability
* jun 2021 - error proportional power for tracking movement. Real Soft-Stop
* aug 2021 - faster USB update, cold switching Az/El direction, small optimizations in the code
* nov 2021 - cracked the secret of Soft-Start. It wasn't hard. There you have it
*/
#include <Wire.h> // Library for I2C communication
#include <LiquidCrystal_I2C.h> // Library for LCD
// Wiring: SDA pin is connected to A4 and SCL pin to A5.
// Connect to LCD via I2C, default address 0x27 (A0-A2 not jumpered)
LiquidCrystal_I2C lcd(0x27, 16, 2); // address, chars, rows.
// declaring custom symbol for up/down arrow
byte DownArrow[8] = {
B00000,
B00100,
B00100,
B00100,
B10101,
B01110,
B00100,
B00000
};
byte UpArrow[8] = {
B00000,
B00100,
B01110,
B10101,
B00100,
B00100,
B00100,
B00000
};
/***********************************THIS IS WHERE YOU REALY TWEAK THE ANTENNA MOVEMENT***************/
// ANTENNA potentiometers CALIBRATION
int AzMin = 1; //begining of the potentiometer
int AzMax = 1023; //end of the potentiometer
int ElMin = 1;
int ElMax = 1023;
// Allowed error for which antennna won't move
int AzErr = 8;
int ElErr = 4;
// Angle difference where soft stop begins
int Amax = 25; //azimuth
int Emax = 15; //elevation
// min and max power for motors, percents;
int PwAzMin = 30; //minimum power for which the motor doesn't stall and starts under load
int PwAzMax = 100; //full power for the fastest speed
int PwElMin = 30;
int PwElMax = 100;
int PwAz = 0; //calculated power to be transmitted to motor (percents);
int PwEl = 0;
/***************************************************************************************************/
// Encoder variables
enum AzPinAssignments {
AzEncoderPinA = 2, // Az encoder right
AzEncoderPinB = 3, // encoder left
AzClearButton = 4, // encoder push
ElEncoderPinA = 6, // El encoder right
ElEncoderPinB = 5}; // encoder left
// interrupt service routine vars
unsigned int lastReportedPos = 1; // change management
static boolean rotating = false; // debounce management
boolean A_set = false;
boolean B_set = false;
int aState;
int aLastState;
// other variables
int AzPotPin = A0; // select the input pin for the azim. potentiometer
int AzRotPin = 12; // select the out pin for rotation direction
int AzPWMPin = 11; // select the out pin for azimuth PWM command
int TruAzim = 0; // calculated real azimuth value
int ComAzim = 0; // commanded azimuth value
int OldTruAzim = 0; // to store previous azimuth value
int OldComAzim = 0;
char AzDir; // symbol for azim rot display
int AzEncBut = 1; // variable to toggle with encoder push button
int ElPotPin = A1; // select the input pin for the elev. potentiometer
int ElRotPin = 13; // select the out pin for elevation rotation direction
int ElPWMPin = 10; // select the out pin for elevation rotation PWM command
int TruElev = 0; // calculated real elevation value
int ComElev = 0; // commanded elevation value
int OldTruElev = 0; // to store previous elevation value
int OldComElev = 0;
char ElDir; // symbol for elev. rot display
// flags for AZ, EL tolerances
bool AzStop = false;
bool ElStop = false;
int ElUp = 1; // 1 - Elevation Dn, 0 - Elevation STOP, 2 - Elevation Up
int StaAzim = 0; // Start Azimuth angle for motor Soft-Start
int PwAzStop = 0; // calculated PWM (percent) for soft-stop
int PwAzStar = 0; // calculated PWM (percent) for soft-start
int StaElev = 0; // Start Elevation angle for motor Soft-Start
int PwElStop = 0; // calculated PWM (percent) for soft-stop
int PwElStar = 0; // calculated PWM (percent) for soft-start
//averaging loop
const int numReadings = 25;
int readIndex = 0; // the index of the current reading
int azimuth[numReadings]; // the readings from the analog input
int elevation[numReadings];
int totalAz = 0; // the running total
int totalEl = 0;
// variables for serial comm
String Azimuth = "";
String Elevation = "";
String ComputerRead;
String ComputerWrite;
bool AZser = false;
bool ELser = false;
bool ANTser = false;
/*************** END VARIABLE DECLARATION ************/
void setup() {
Serial.begin(9600);
Serial.setTimeout(50); // miliseconds to wait for USB sata. Default 1000
// Initiate the LCD:
// lcd.begin(16,2); //select this one if the arrows are not displayed correctly
lcd.init();
lcd.backlight();
// write on display name and version
lcd.setCursor(0, 0); // Set the cursor on the first column first row.(counting starts at 0!)
lcd.print("EasyCom AntRotor"); // display "..."
lcd.setCursor(0, 1); // Set the cursor on the first column the second row
lcd.print("*Racov* Nov.2021");
//creating custom symbol for up/dwn arrow
lcd.createChar(1, DownArrow);
lcd.createChar(2, UpArrow);
// pin declaration
pinMode(AzRotPin, OUTPUT); //declaring azim. rotation direction Pin as OUTPUT
pinMode(AzPWMPin, OUTPUT); //declaring azimuth PWM command Pin as OUTPUT
pinMode(ElRotPin, OUTPUT); //declaring elev. rotation direction Pin as OUTPUT
pinMode(ElPWMPin, OUTPUT);
pinMode(AzPotPin, INPUT);
pinMode(ElPotPin, INPUT);
pinMode(AzEncoderPinA, INPUT);
pinMode(AzEncoderPinB, INPUT);
pinMode(AzClearButton, INPUT);
pinMode(ElEncoderPinA, INPUT);
pinMode(ElEncoderPinB, INPUT);
// AzEncoder pin on interrupt 0 (pin A)
attachInterrupt(0, doEncoderA, CHANGE);
// AzEncoder pin on interrupt 1 (pin B)
attachInterrupt(1, doEncoderB, CHANGE);
// Reads the initial state of the ElEncoderPinA
aLastState = digitalRead(ElEncoderPinA);
/* initialization of the averaging loop */
TruAzim = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359)); // azimuth value 0-359
if (TruAzim<0) {TruAzim=0;}
if (TruAzim>359) {TruAzim=359;} // keep values between limits
TruElev = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90)); // elev value 0-90
if (TruElev<0) {TruElev=0;}
if (TruElev>90) {TruElev=90;} // keep values between limits
for (int thisReading = 0; thisReading < numReadings; thisReading++) {
azimuth[thisReading] = TruAzim;
elevation[thisReading] = TruElev;
}
totalAz = TruAzim * numReadings;
totalEl = TruElev * numReadings;
// this is to set azim-command the same value as real, not to jerk the antenna at start-up
ComAzim = TruAzim;
ComElev = TruElev;
OldTruAzim = TruAzim;
OldComAzim = ComAzim;
OldTruElev = TruElev;
OldComElev = TruElev;
delay(1500); // keep for 1.5 seconds
// display Azim. and Elev. values
lcd.setCursor(0, 0);
lcd.print("Azm.---" + String(char(223)) + "=Cd.---" + String(char(223))); // char(223) is degree symbol
lcd.setCursor(0, 1);
lcd.print("Elv. --" + String(char(223)) + "=Cd. --" + String(char(223)));
DisplAzim(TruAzim, 4,0);
DisplAzim(ComAzim,12,0);
DisplElev(TruElev, 5,1);
DisplElev(ComElev,13,1);
}
// end SETUP
void loop() {
/************** FYI, this loop repeats 500 times per second !!! **************/
// AZIMUTH/ELEVATION AVERAGING LOOP
// subtract the oldest value
totalAz = totalAz - azimuth[readIndex];
totalEl = totalEl - elevation[readIndex];
// read from the sensor:
azimuth[readIndex] = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359));
elevation[readIndex] = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90));
// add the reading to the total:
totalAz = totalAz + azimuth[readIndex];
totalEl = totalEl + elevation[readIndex];
// do the average
TruAzim = totalAz / numReadings;
TruElev = totalEl / numReadings;
// keep values between limits
if (TruAzim<0) {TruAzim=0;}
if (TruAzim>359) {TruAzim=359;}
if (TruElev<0) {TruElev=0;}
if (TruElev>90) {TruElev=90;}
// advance to the next position in the array:
readIndex = readIndex + 1;
// if we're at the end of the array, wrap around to the beginning:
if (readIndex >= numReadings) {readIndex = 0;}
// this is to read the command from encoder
ReadAzimEncoder();
ReadElevEncoder();
if (Serial.available()) {SerComm();} // read USB data
// update antenna position display only if value change
if ((millis()%500)<10){ //not to flicker the display
if (OldTruAzim!=TruAzim) {
DisplAzim(TruAzim,4,0);
OldTruAzim = TruAzim;
}
if (OldTruElev!=TruElev) {
DisplElev(TruElev,5,1);
OldTruElev = TruElev;
}
}
// update target position display only if value change
if (OldComAzim != ComAzim) {
DisplAzim(ComAzim,12,0);
OldComAzim = ComAzim;
}
if (OldComElev != ComElev) {
DisplElev(ComElev,13,1);
OldComElev = ComElev;
}
// this is to rotate in azimuth
if (TruAzim == ComAzim) { // if equal, stop moving
AzStop = true;
analogWrite(AzPWMPin, 0); // Az motor power = 0
StaAzim = TruAzim; // this will be the start azimuth for soft-start
lcd.setCursor(8, 0);
lcd.print("=");
}
else if ((abs(TruAzim - ComAzim)<=AzErr)&&(AzStop == false)) { // if in tolerance, but it wasn't an equal, rotate
AzimRotate();}
else if (abs(TruAzim - ComAzim)>AzErr){ // if target is off tolerance
AzStop = false; // it's not equal
AzimRotate(); // rotate
}
// this is to rotate in elevation
if (TruElev == ComElev) { // if equal, stop moving
ElStop = true;
analogWrite(ElPWMPin, 0); // El motor power = 0
StaElev = TruElev; // this will be the start elevation for soft-start
lcd.setCursor(8, 1);
lcd.print("=");
ElUp = 0; // flag for elevation STOP
}
else if ((abs(TruElev - ComElev)<=ElErr)&&(ElStop == false)) { // if in tolerance, but it wasn't an equal, rotate
ElevRotate();}
else if (abs(TruElev - ComElev)>ElErr){ // if target is off tolerance
ElStop = false; // it's not equal
ElevRotate(); // rotate
}
// this is to interpret Az encoder x10 multiplication
while (AzEncBut == 10) { // while toggled to x10
analogWrite(AzPWMPin, 0); // STOP antenna rotation
StaAzim = TruAzim; // this will be the start azimuth for soft-start
analogWrite(ElPWMPin, 0);
lcd.setCursor(8, 0);
lcd.print("*");
ReadAzimEncoder();
if (OldComAzim != ComAzim){ // update display only if numbers change
DisplAzim(ComAzim, 12, 0);
OldComAzim = ComAzim;
}
delay (100);
}
}
// end main LOOP
//____________________________________________________
// ___________procedures definitions__________________
void DisplAzim(int x, int y, int z) {
char displayString[7] = "";
sprintf(displayString, "%03d", x); //outputs a fixed lenght number (3 integer)
lcd.setCursor(y, z); // for no leading zeros "__7" use "%3d"
lcd.print(displayString);
// ************** FOR CALIBRATION PURPOSES **************
// Serial.print ("Az ");
// Serial.println (analogRead(AzPotPin));
}
void DisplElev(int x, int y, int z){
char displayString[7] = "";
sprintf(displayString, "%02d", x); //outputs a fixed lenght number (2 integer)
lcd.setCursor(y, z); // for no leading zeros "_7" use "%2d"
lcd.print(displayString);
// ************** FOR CALIBRATION PURPOSES **************
// Serial.print ("El ");
// Serial.println (analogRead(ElPotPin));
}
void ReadElevEncoder() {
aState = digitalRead(ElEncoderPinA); // Reads the "current" state of the ElEncoderPinA
// If the previous and the current state of the ElEncoderPinA are different, that means a Pulse has occured
if (aState != aLastState){
// If the ElEncoderPinB state is different to the ElEncoderPinA state, that means the encoder is rotating clockwise
if (digitalRead(ElEncoderPinB) != aState) { ComElev ++;}
else { ComElev --;}
if (ComElev <0) {ComElev = 0;}
if (ComElev >90) {ComElev = 90;}
}
aLastState = aState; // Updates the previous state of the ElEncoderPinA with the current state
}
void ReadAzimEncoder() {
rotating = true; // reset the debouncer
if (lastReportedPos != ComAzim) {
lastReportedPos = ComAzim;
}
delay(10);
if (digitalRead(AzClearButton) == LOW ) { // if encoder switch depressed
delay (250); // debounce switch
if (AzEncBut == 1){
AzEncBut = 10;
ComAzim = int(ComAzim/10)*10; // ComAzim in 10deg. steps
}
else {
AzEncBut = 1;
}
}
} //end ReadAzimEncoder()
// Interrupt on A changing state
void doEncoderA() {
// debounce
if ( rotating ) delay (1); // wait a little until the bouncing is done
// Test transition, did things really change?
if ( digitalRead(AzEncoderPinA) != A_set ) { // debounce once more
A_set = !A_set;
// adjust counter + if A leads B
if ( A_set && !B_set )
ComAzim += AzEncBut;
ComAzim = ((ComAzim + 360) % 360); // encoderPos between 0 and 359 deg.
rotating = false; // no more debouncing until loop() hits again
}
}
// Interrupt on B changing state, same as A above
void doEncoderB() {
if ( rotating ) delay (1);
if ( digitalRead(AzEncoderPinB) != B_set ) {
B_set = !B_set;
// adjust counter - 1 if B leads A
if ( B_set && !A_set )
ComAzim -= AzEncBut;
ComAzim = ((ComAzim + 360) % 360); // encoderPos between 0 and 359 deg.
rotating = false;
}
}
void AzimRotate() {
if (ComAzim > TruAzim) { // this to determine direction of rotation
// cold switching - stop motor before changing direction - to protect mechanic and electric parts
if (AzDir == char(127)) { // if previously rotating in the oposite direction
analogWrite(AzPWMPin, 0); // STOP the motor
StaAzim = TruAzim; // this will be the start azimuth for soft-start
delay(200); // pre-switch delay
digitalWrite(AzRotPin, LOW); // deactivate rotation pin - rotate right
delay(200); // post-switch delay
}
else { // same directin, no Stop, no delay
digitalWrite(AzRotPin, LOW); // deactivate rotation pin - rotate right
}
AzDir = char(126); // "->"
}
else {
if (AzDir == char(126)) { // if previously rotating in the oposite direction
analogWrite(AzPWMPin, 0); // STOP the motor
StaAzim = TruAzim; // this will be the start azimuth for soft-start
delay(200); // pre-switch delay
digitalWrite(AzRotPin, HIGH); // activate rotation pin - rotate left
delay(200); // post-switch delay
}
else { // same directin, no Stop, no delay
digitalWrite(AzRotPin, HIGH); // activate rotation pin - rotate left
}
AzDir = char(127); // "<-"
}
lcd.setCursor(8, 0);
lcd.print(String(AzDir));
// this activates azim PWM pin proportional with angle error (calculated in percents %)
PwAzStop = PwAzMin + round((abs(ComAzim-TruAzim))*(PwAzMax-PwAzMin)/Amax); //formula which outputs a power proportional with angle difference for Soft-Stop
PwAzStar = PwAzMin + round((abs(StaAzim-TruAzim))*(PwAzMax-PwAzMin)/Amax); //formula which outputs a power proportional with angle difference for Soft-Start
if (PwAzStar > PwAzStop){
PwAz = PwAzStop; //choose whichever value is smallest
}
else {PwAz = PwAzStar;}
if (PwAz > PwAzMax) {PwAz = PwAzMax;}
analogWrite(AzPWMPin, round(2.55*PwAz)); // activate Azim drive PWM pin
}
// end AzimRotate()
void ElevRotate() {
// this to determine direction of rotation
if (ComElev > TruElev) {
if (ElUp == 1) { // if previously rotating in the oposite direction
analogWrite(ElPWMPin, 0); // STOP the motor
StaElev = TruElev; // this will be the start elevation for soft-start
delay(200); // pre-switch delay
digitalWrite(ElRotPin, LOW); // deactivate rotation pin - rotate UP
delay(200); // post-switch delay
}
else { // same directin, no Stop, no delay
digitalWrite(ElRotPin, LOW); // deactivate rotation pin - rotate UP
}
lcd.setCursor(8, 1);
lcd.write(2); // arrow up
ElUp = 2; // flag for elevation UP
}
else {
if (ElUp == 2) { // if previously rotating in the oposite direction
analogWrite(ElPWMPin, 0); // STOP the motor
StaElev = TruElev; // this will be the start elevation for soft-start
delay(200); // pre-switch delay
digitalWrite(ElRotPin, HIGH); // deactivate rotation pin - rotate UP
delay(200); // post-switch delay
}
else { // same directin, no Stop, no delay
digitalWrite(ElRotPin, HIGH); // deactivate rotation pin - rotate UP
}
lcd.setCursor(8, 1);
lcd.write(1); // arrow down
ElUp = 1; // flag for elevation DN
}
// this activates azim PWM pin proportional with angle error (calculated in percents %)
PwElStop = PwElMin + round((abs(ComElev-TruElev))*(PwElMax-PwElMin)/Emax); //formula which outputs a power proportional with angle difference for Soft-Stop
PwElStar = PwElMin + round((abs(StaElev-TruElev))*(PwElMax-PwElMin)/Emax); //formula which outputs a power proportional with angle difference for Soft-Start
if (PwElStar > PwElStop){
PwEl = PwElStop; //choose whichever value is smallest
}
else {PwEl = PwElStar;}
if (PwEl > PwElMax) {PwEl = PwElMax;}
analogWrite(ElPWMPin, round(2.55*PwEl)); // activate Elev drive PWM pin
}
// end ElevRotate()
void SerComm() {
// initialize readings
ComputerRead = "";
Azimuth = "";
Elevation = "";
while(Serial.available()) {
ComputerRead= Serial.readString(); // read the incoming data as string
// Serial.println(ComputerRead); // echo the reception for testing purposes
}
// looking for command <AZxxx.x>
for (int i = 0; i <= ComputerRead.length(); i++) {
if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')){ // if read AZ
for (int j = i+2; j <= ComputerRead.length(); j++) {
if (isDigit(ComputerRead.charAt(j))) { // if the character is number
Azimuth = Azimuth + ComputerRead.charAt(j);
}
else {break;}
}
}
}
// looking for command <ELxxx.x>
for (int i = 0; i <= (ComputerRead.length()-2); i++) {
if ((ComputerRead.charAt(i) == 'E')&&(ComputerRead.charAt(i+1) == 'L')){ // if read EL
if ((ComputerRead.charAt(i+2)) == '-') {
ComElev = 0; // if elevation negative
break;
}
for (int j = i+2; j <= ComputerRead.length(); j++) {
if (isDigit(ComputerRead.charAt(j))) { // if the character is number
Elevation = Elevation + ComputerRead.charAt(j);
}
else {break;}
}
}
}
// if <AZxx> received
if (Azimuth != ""){
ComAzim = Azimuth.toInt();
ComAzim = ComAzim%360; // keeping values between limits(for trackers with more than 360 deg. rotation)
}
// if <ELxx> received
if (Elevation != ""){
ComElev = Elevation.toInt();
if (ComElev>180) { ComElev = 0;}
if (ComElev>90) { //if received more than 90deg. (for trackers with 180deg. elevation)
ComElev = 180-ComElev; //keep below 90deg.
ComAzim = (ComAzim+180)%360; //and rotate the antenna on the back
}
}
// looking for <AZ EL> interogation for antenna position
for (int i = 0; i <= (ComputerRead.length()-4); i++) {
if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')&&(ComputerRead.charAt(i+3) == 'E')&&(ComputerRead.charAt(i+4) == 'L')){
// send back the antenna position <+xxx.x xx.x>
ComputerWrite = "+"+String(TruAzim)+".0 "+String(TruElev)+".0";
Serial.println(ComputerWrite);
}
}
}
// end SerComm()
ant-rot-AC-aug2021Arduino
Make sure you use the Electric Diagram for AC motorsOffers dry contacts (ON/OFF). It can be easily interfaced with commercial rotators.
/* AZ/EL Antenna Rotator controller for Arduino - AC motors
* ========================================================
* Uses EasyComm protocol for computer - Tracking Software
* Manual command by means of two rotary encoders AZ - EL
*
* compatible with switch-box rotators
* or AC motors
* dry contatcts for Left-Right, Up-Down
*
* Viorel Racoviteannu /
* https://www.youtube.com/channel/UCiRLZX0bV9rS04BGAyUf-fA
* https://racov.ro
* YO3RAK@gmail.com
*
* I cannot take any responsibility for missuse of this code
* or any kind of damage it may occur from using this code.
*
* dec 2020 v2 - improved serial comm stability
* jan 2021 - fixed AZ, EL tolerances for motor activation
* apr 2021 - improved serial comm stability
* aug 2021 - faster USB update, cold switching Az/El direction, small optimizations in the code
*/
#include <Wire.h> // Library for I2C communication
#include <LiquidCrystal_I2C.h> // https://www.arduinolibraries.info/libraries/liquid-crystal-i2-c (Library for LCD)
// Wiring: SDA pin is connected to A4 and SCL pin to A5.
// Connect to LCD via I2C, default address 0x27 (A0-A2 not jumpered)
LiquidCrystal_I2C lcd(0x27, 16, 2); // address, chars, rows.
// declaring custom symbol for up/down arrow
byte DownArrow[8] = {
B00000,
B00100,
B00100,
B00100,
B10101,
B01110,
B00100,
B00000
};
byte UpArrow[8] = {
B00000,
B00100,
B01110,
B10101,
B00100,
B00100,
B00100,
B00000
};
// ANTENNA potentiometers CALIBRATION
int AzMin = 1; //begining of the potentiometer
int AzMax = 1023; //end of the potentiometer
int ElMin = 1;
int ElMax = 1023;
// Allowed error for which antenna won't move
int AzErr = 8;
int ElErr = 4;
// Azim encoder variables
enum AzPinAssignments {
AzEncoderPinA = 2, // encoder right
AzEncoderPinB = 3, // encoder left
AzClearButton = 4}; // encoder push
unsigned int lastReportedPos = 1; // change management
static boolean rotating = false; // debounce management
// interrupt service routine vars
boolean A_set = false;
boolean B_set = false;
//Elev encoder variables
enum ElPinAssignments{
ElEncoderPinA = 6, // encoder right
ElEncoderPinB = 5, // encoder left
ElClearButton = 7}; // encoder push
int aState;
int aLastState;
// other variables
int AzPotPin = A0; // select the input pin for the azim. potentiometer
int AzRotPinR = 13; // select the out pin for rotation direction
int AzRotPinL = 12;
int TruAzim = 0; // calculated real azimuth value
int ComAzim = 0; // commanded azimuth value
int OldTruAzim = 0; // to store previous azimuth value
int OldComAzim = 0;
char AzDir; // symbol for azim rot display
int AzEncBut = 1; // variable to toggle with encoder push button
int ElPotPin = A1; // select the input pin for the elev. potentiometer
int ElRotPinD = 11; // select the out pin for elevation rotation direction
int ElRotPinU = 10;
int TruElev = 0; // calculated real elevation value
int ComElev = 0; // commanded elevation value
int OldTruElev = 0; // to store previous elevation value
int OldComElev = 0;
char ElDir; // symbol for elev. rot display
int ElEncBut = 1; // variable to toggle with encoder push button
// flags for AZ, EL tolerances
bool AzStop = false;
bool ElStop = false;
int ElUp = 0; // 1 = Elevation Dn, 0 = Elevation STOP, 2 = Elevation Up
//averaging loop
const int numReadings = 25;
int readIndex = 0; // the index of the current reading
int azimuth[numReadings]; // the readings from the analog input
int elevation[numReadings];
int totalAz = 0; // the running total
int totalEl = 0;
// variables for serial comm
String Azimuth = "";
String Elevation = "";
String ComputerRead;
String ComputerWrite;
bool AZser = false;
bool ELser = false;
bool ANTser = false;
/*************** END VARIABLE DECLARATION ************/
void setup() {
Serial.begin(9600);
Serial.setTimeout(50); // miliseconds to wait for USB sata. Default 1000
// Initiate the LCD:
// lcd.begin(16,2); //select this one if the arrows are not displayed correctly
lcd.init();
lcd.backlight();
// write on display name and version
lcd.setCursor(0, 0); // Set the cursor on the first column first row.(counting starts at 0!)
lcd.print("EasyCom AntRotor");
lcd.setCursor(0, 1); // Set the cursor on the first column the second row
lcd.print("*Racov* Aug.2021 ");
//creating custom symbol for up/dwn arrow
lcd.createChar(1, DownArrow);
lcd.createChar(2, UpArrow);
// pin declaration
pinMode(AzRotPinR, OUTPUT); //declaring azim. rotation direction Pin as OUTPUT
pinMode(AzRotPinL, OUTPUT);
pinMode(ElRotPinD, OUTPUT); //declaring elev. rotation direction Pin as OUTPUT
pinMode(ElRotPinU, OUTPUT);
pinMode(AzPotPin, INPUT);
pinMode(ElPotPin, INPUT);
pinMode(AzEncoderPinA, INPUT);
pinMode(AzEncoderPinB, INPUT);
pinMode(AzClearButton, INPUT);
pinMode(ElEncoderPinA, INPUT);
pinMode(ElEncoderPinB, INPUT);
pinMode(ElClearButton, INPUT);
// AzEncoder pin on interrupt 0 (pin A)
attachInterrupt(0, doEncoderA, CHANGE);
// AzEncoder pin on interrupt 1 (pin B)
attachInterrupt(1, doEncoderB, CHANGE);
// Reads the initial state of the ElEncoderPinA
aLastState = digitalRead(ElEncoderPinA);
/* initialization of the averaging loop */
TruAzim = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359)); // azimuth value 0-359
if (TruAzim<0) {TruAzim=0;}
if (TruAzim>359) {TruAzim=359;} // keep values between limits
TruElev = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90)); // elev value 0-90
if (TruElev<0) {TruElev=0;}
if (TruElev>90) {TruElev=90;} // keep values between limits
for (int thisReading = 0; thisReading < numReadings; thisReading++) {
azimuth[thisReading] = TruAzim;
elevation[thisReading] = TruElev;
}
totalAz = TruAzim * numReadings;
totalEl = TruElev * numReadings;
// this is to set azim-command the same value as real, not to jerk the antenna at start-up
ComAzim = TruAzim;
ComElev = TruElev;
OldTruAzim = TruAzim;
OldComAzim = ComAzim;
OldTruElev = TruElev;
OldComElev = TruElev;
delay(1500); // keep for 1.5 seconds
// display Azim. and Elev. values
lcd.setCursor(0, 0);
lcd.print("Azm.---" + String(char(223)) + "=Cd.---" + String(char(223))); // char(223) is degree symbol
lcd.setCursor(0, 1);
lcd.print("Elv. --" + String(char(223)) + "=Cd. --" + String(char(223)));
DisplAzim(TruAzim, 4,0);
DisplAzim(ComAzim,12,0);
DisplElev(TruElev, 5,1);
DisplElev(ComElev,13,1);
}
// end SETUP
void loop() {
/************** FYI, this loop repeats 500 times per second !!! **************/
// AZIMUTH/ELEVATION AVERAGING LOOP
// subtract the oldest value
totalAz = totalAz - azimuth[readIndex];
totalEl = totalEl - elevation[readIndex];
// read from the sensor:
azimuth[readIndex] = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359));
elevation[readIndex] = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90));
// add the reading to the total:
totalAz = totalAz + azimuth[readIndex];
totalEl = totalEl + elevation[readIndex];
// do the average
TruAzim = totalAz / numReadings;
TruElev = totalEl / numReadings;
// keep values between limits
if (TruAzim<0) {TruAzim=0;}
if (TruAzim>359) {TruAzim=359;}
if (TruElev<0) {TruElev=0;}
if (TruElev>90) {TruElev=90;}
// advance to the next position in the array:
readIndex = readIndex + 1;
// if we're at the end of the array, wrap around to the beginning:
if (readIndex >= numReadings) {readIndex = 0;}
// this is to read the command from encoder
ReadAzimEncoder();
ReadElevEncoder();
if (Serial.available()) {SerComm();} // read USB data
// update antenna position display only if value change
if ((millis()%500)<10){ //not to flicker the display
if (OldTruAzim!=TruAzim) {
DisplAzim(TruAzim,4,0);
OldTruAzim = TruAzim;
}
if (OldTruElev!=TruElev) {
DisplElev(TruElev,5,1);
OldTruElev = TruElev;
}
}
// update target position display only if value change
if (OldComAzim != ComAzim) {
DisplAzim(ComAzim,12,0);
OldComAzim = ComAzim;
}
if (OldComElev != ComElev) {
DisplElev(ComElev,13,1);
OldComElev = ComElev;
}
// this is to rotate in azimuth
if (TruAzim == ComAzim) { // if equal, stop moving
AzStop = true;
digitalWrite(AzRotPinL, LOW); // deactivate rotation pin
digitalWrite(AzRotPinR, LOW);
lcd.setCursor(8, 0);
lcd.print("=");
}
else if ((abs(TruAzim - ComAzim)<=AzErr)&&(AzStop == false)) { // if in tolerance, but it wasn't an equal, rotate
AzimRotate();}
else if (abs(TruAzim - ComAzim)>AzErr){ // if target is off tolerance
AzStop = false; // it's not equal
AzimRotate(); // rotate
}
// this is to rotate in elevation
if (TruElev == ComElev) { // if equal, stop moving
ElStop = true;
digitalWrite(ElRotPinD, LOW); // deactivate elevator pin
digitalWrite(ElRotPinU, LOW);
lcd.setCursor(8, 1);
lcd.print("=");
ElUp = 0; // flag for elevation STOP
}
else if ((abs(TruElev - ComElev)<=ElErr)&&(ElStop == false)) { // if in tolerance, but it wasn't an equal, rotate
ElevRotate();}
else if (abs(TruElev - ComElev)>ElErr){ // if target is off tolerance
ElStop = false; // it's not equal
ElevRotate(); // rotate
}
// this is to interpret x10 AZ ENC multiplication
while (AzEncBut == 10) { // while toggled to x10
digitalWrite(AzRotPinL, LOW); // deactivate rotation pin
digitalWrite(AzRotPinR, LOW);
digitalWrite(ElRotPinD, LOW); // deactivate elevator pin
digitalWrite(ElRotPinU, LOW);
lcd.setCursor(8, 0);
lcd.print("*");
ReadAzimEncoder();
if (OldComAzim != ComAzim){ // update display only if numbers change
DisplAzim(ComAzim, 12, 0);
OldComAzim = ComAzim;
}
delay(100);
}
}
// end main LOOP
//____________________________________________________
// ___________procedures definitions__________________
void DisplAzim(int x, int y, int z) {
char displayString[7] = "";
sprintf(displayString, "%03d", x); //outputs a fixed lenght number (3 integer)
lcd.setCursor(y, z); // for no leading zeros "__7" use "%3d"
lcd.print(displayString);
// ************** FOR CALIBRATION PURPOSES **************
// Serial.print ("Az ");
// Serial.println (analogRead(AzPotPin));
}
void DisplElev(int x, int y, int z){
char displayString[7] = "";
sprintf(displayString, "%02d", x); //outputs a fixed lenght number (2 integer)
lcd.setCursor(y, z); // for no leading zeros "_7" use "%2d"
lcd.print(displayString);
// ************** FOR CALIBRATION PURPOSES **************
// Serial.print ("El ");
// Serial.println (analogRead(ElPotPin));
}
void ReadElevEncoder() {
aState = digitalRead(ElEncoderPinA); // Reads the "current" state of the ElEncoderPinA
// If the previous and the current state of the ElEncoderPinA are different, that means a Pulse has occured
if (aState != aLastState){
// If the ElEncoderPinB state is different to the ElEncoderPinA state, that means the encoder is rotating clockwise
if (digitalRead(ElEncoderPinB) != aState) { ComElev ++;}
else { ComElev --;}
if (ComElev <0) {ComElev = 0;}
if (ComElev >90) {ComElev = 90;}
}
aLastState = aState; // Updates the previous state of the ElEncoderPinA with the current state
}
void ReadAzimEncoder() {
rotating = true; // reset the debouncer
if (lastReportedPos != ComAzim) {
lastReportedPos = ComAzim;
}
delay(10);
if (digitalRead(AzClearButton) == LOW ) { // if encoder switch depressed
delay (250); // debounce switch
if (AzEncBut == 1){
AzEncBut = 10;
ComAzim = int(ComAzim/10)*10; // ComAzim in 10deg. steps
}
else {
AzEncBut = 1;
}
}
} //end ReadAzimEncoder()
// Interrupt on A changing state
void doEncoderA() {
// debounce
if ( rotating ) delay (1); // wait a little until the bouncing is done
// Test transition, did things really change?
if ( digitalRead(AzEncoderPinA) != A_set ) { // debounce once more
A_set = !A_set;
// adjust counter + if A leads B
if ( A_set && !B_set )
ComAzim += AzEncBut;
ComAzim = ((ComAzim + 360) % 360); // encoderPos between 0 and 359 deg.
rotating = false; // no more debouncing until loop() hits again
}
}
// Interrupt on B changing state, same as A above
void doEncoderB() {
if ( rotating ) delay (1);
if ( digitalRead(AzEncoderPinB) != B_set ) {
B_set = !B_set;
// adjust counter - 1 if B leads A
if ( B_set && !A_set )
ComAzim -= AzEncBut;
ComAzim = ((ComAzim + 360) % 360); // encoderPos between 0 and 359 deg.
rotating = false;
}
}
void AzimRotate() {
if ((ComAzim-TruAzim) > (TruAzim-ComAzim)) { // this to determine direction of rotation
// cold switching - stop motor before changing direction - to protect mechanic and electric parts
digitalWrite(AzRotPinL, LOW); // deactivate rotation pin Left
if (AzDir == char(127)) {delay(500);} // if previously rotating in the oposite direction, wait 0.5 seconds
digitalWrite(AzRotPinR, HIGH); // activate rotation pin Right
AzDir = char(126); // "->"
}
else {
digitalWrite(AzRotPinR, LOW);
if (AzDir == char(126)) {delay(500);}
digitalWrite(AzRotPinL, HIGH);
AzDir = char(127); // "<-"
}
lcd.setCursor(8, 0);
lcd.print(String(AzDir));
}
void ElevRotate() {
// this to determine direction of rotation
if ((ComElev-TruElev) > (TruElev-ComElev)) {
digitalWrite(ElRotPinD, LOW);
if (ElUp == 1) {delay(500);}
digitalWrite(ElRotPinU, HIGH);
lcd.setCursor(8, 1);
lcd.write(2); // arrow up
ElUp = 2;
}
else {
digitalWrite(ElRotPinU, LOW);
if (ElUp == 2) {delay(500);}
digitalWrite(ElRotPinD, HIGH);
lcd.setCursor(8, 1);
lcd.write(1); // arrow down
ElUp = 1;
}
}
void SerComm() {
// initialize readings
ComputerRead = "";
Azimuth = "";
Elevation = "";
while(Serial.available()) {
ComputerRead= Serial.readString(); // read the incoming data as string
Serial.println(ComputerRead); // echo the reception for testing purposes
}
// looking for command <AZxxx.x>
for (int i = 0; i <= ComputerRead.length(); i++) {
if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')){ // if read AZ
for (int j = i+2; j <= ComputerRead.length(); j++) {
if (isDigit(ComputerRead.charAt(j))) { // if the character is number
Azimuth = Azimuth + ComputerRead.charAt(j);
}
else {break;}
}
}
}
// looking for command <ELxxx.x>
for (int i = 0; i <= (ComputerRead.length()-2); i++) {
if ((ComputerRead.charAt(i) == 'E')&&(ComputerRead.charAt(i+1) == 'L')){ // if read EL
if ((ComputerRead.charAt(i+2)) == '-') {
ComElev = 0; // if elevation negative
break;
}
for (int j = i+2; j <= ComputerRead.length(); j++) {
if (isDigit(ComputerRead.charAt(j))) { // if the character is number
Elevation = Elevation + ComputerRead.charAt(j);
}
else {break;}
}
}
}
// if <AZxx> received
if (Azimuth != ""){
ComAzim = Azimuth.toInt();
ComAzim = ComAzim%360; // keeping values between limits
}
// if <ELxx> received
if (Elevation != ""){
ComElev = Elevation.toInt();
if (ComElev>180) { ComElev = 0;}
if (ComElev>90) { //if received more than 90deg. (for trackers with 180deg. elevation)
ComElev = 180-ComElev; //keep below 90deg.
ComAzim = (ComAzim+180)%360; //and rotate the antenna on the back
}
}
// looking for <AZ EL> interogation for antenna position
for (int i = 0; i <= (ComputerRead.length()-4); i++) {
if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')&&(ComputerRead.charAt(i+3) == 'E')&&(ComputerRead.charAt(i+4) == 'L')){
// send back the antenna position <+xxx.x xx.x>
ComputerWrite = "+"+String(TruAzim)+".0 "+String(TruElev)+".0";
Serial.println(ComputerWrite);
}
}
}
// end SerComm()
Potentiometer calibration procedureArduino
AZ / EL Potentiometers limit calibration PROCEDURE for displaying the correct antenna angles and rotation limits ( 0-359ᴼ / 0-90ᴼ)This is plain text, not a code :)
AZ / EL Potentiometers limit calibration PROCEDURE ( 0-359ᴼ / 0-90ᴼ)
This might seem complicated, but it only has to be done once.
1. Open the code in Arduino and
- Look for
void DisplAzim(int x, int y, int z) {
...
// Serial.print ("Az ");
// Serial.println (analogRead(AzPotPin));
- Uncoment these lines. Should look like this:
Serial.print ("Az ");
Serial.println (analogRead(AzPotPin));
- Look for
void DisplElev(int x, int y, int z){
...
// Serial.print ("El ");
// Serial.println (analogRead(ElPotPin));
Uncoment these lines, too. Should look like this:
Serial.print ("El ");
Serial.println (analogRead(ElPotPin));
2. Upload the code and open the Serial Monitor. There you will see a lot of numbers;
3. With the help of the encoders, move the antenna to minimum values, 0ᴼ in azimuth and 0ᴼ in elevation.
- Write down the values for Azimuth and Elevation. (in my case it was AzMin=90, ElMin=10)
- These are the input values read by Arduino, not the real angles;
4. Move the antenna again to maximum values, 359ᴼ in azimuth and 90ᴼ in elevation.
- Again, write down the values for Azimuth and Elevation. (in my case it was AzMax=1000, ElMax=992);
5. Look in the code, at the beginning, for the section
// ANTENNA potentiometers CALIBRATION
int AzMin = 1;
int AzMax = 1023;
int ElMin = 1;
int ElMax = 1023;
- Here input the values you wrote down for each situation;
6. Now it is no longer necessary to send this on serial, so you have to comment back these lines, like this:
// Serial.print ("Az ");
// Serial.println (analogRead(AzPotPin));
...
// Serial.print ("El ");
// Serial.println (analogRead(ElPotPin));
7. Upload again the code.
That's all.
Now, in the serial monitor, there should be no more numbers, and the true antenna position is read correctly.
Motor calibration procedureArduino
This procedure sets the parameters for the Antenna Speed-Up / Slow-Down Zone.This is plain text, not a code :)
Motor Calibration Procedure For Soft-Start / Soft-Stop feature. This procedure sets the parameters for the Antenna Speed-Up / Slow-Down and the Dead-Zone. You basically set how fast and how slow you want the antenna to start and to stop. You also set much the target can move, before the antenna will adjust again. It’s not strictly necessary, only if you don’t like the default settings. Make sure you first apply the Potentiometer Calibration Procedure !!! That one is strictly necessary. Look at the power diagram for a better understanding. ***For Azimuth movement*** -As the antenna starts to move towards the target, is picking up speed, reaching full power after <Amax> degrees difference. -As the antenna closes in to the target, below <Amax> degrees difference, it starts to slow down. <Amax> should be higher for heavier antennas. -The power starts to decrease from <PwAzMax> to <PwAzMin> until the angle difference becomes zero. <PwAzMax> (in percents %) should be 100 for full speed. If you ever think your antenna rotates too fast, you can set a smaller <PwAzMax>. <PwAzMin> (in percents %) is the minimum power for which your motor doesn’t stall and can start under load. The power output never falls below this value. -Once the antenna reaches the target position (zero degrees error), it stops and doesn’t move again until the target travels more than <AzErr> degrees. This is a dead zone, to prevent continuously shaking the antenna for the smallest target movement, or potentiometer position jitter. The smaller the <AzErr>, the more precise tracking, the more frequent shacking of the motors. ***For Elevation movement*** Exactly as for the Azimuth. Look at the beginning of the code for this section. Here you can input your desired values. /**************THIS IS WHERE YOU REALY TWEAK THE ANTENNA MOVEMENT************/ ... // Allowed error for which antennna won't move. int AzErr = 8; int ElErr = 4; // Angle difference where soft stop begins int Amax = 25; //azimuth int Emax = 15; //elevation // min and max power for motors, percents; int PwAzMin = 30; //minimum power for which the motor doesn't stall and starts under load int PwAzMax = 100; //full power for the fastest speed int PwElMin = 30; int PwElMax = 100; /****************************************************************************/
Schematics
Make sure you use this diagram with the code for DC motors.Connection of all the modules, encoders, LCD, relays, MosFet etc,
Make sure you use this diagram with the code for AC motors.Offers dry contacts (ON/OFF). It can be easily interfaced with commercial rotators.

Manufacturing process
- Control LED Brightness with PWM via Push‑Button on Arduino Mega
- Build a Real-Time Gyroscope Game with Arduino Nano & MPU-6050 Sensor
- DHT11 Temperature & Humidity Sensor Project with LED Indicators and Piezo Speaker
- Unopad: Seamless Arduino MIDI Controller for Ableton Live
- Build a Simple Obstacle Sensor with Arduino – Easy IR LED & Photodiode Tutorial
- Find Me: Smart Item Locator with Arduino and Bluetooth
- Build a 4x4x4 LED Cube with Arduino Uno & 1Sheeld – Interactive LED Project
- Build a Custom Arduino Joystick Steering Wheel for Gaming
- Master Modbus on Arduino: Step‑by‑Step Guide
- Real-Time Soil Moisture Monitoring with LCD Display – Arduino DIY Kit





