Star Tracker

Programación

The code required for this project is organized in blocks, for motor control, sensor reading, battery status reading and user interface management (button and sounds).

Motor

I started programming, with a Arduino Uno, designing small examples for control the bipolar stepper motor. Lacking a driver for this type of motor, I should send the correct signals to each motor coil at every precise moment to get the shaft rotating in an orderly manner. I thought about spending libraries available for it, but because I needed to control the time between step and step I finally ruled out its use.

I ended up implementing the basic code to rotate the engine in both full steps and in half steps. With a loop, a steps counter and a pause between each step, I managed to rotate the motor at the right speed thanks to the calculations made earlier.

With this basic program prototype I decided to do a test to validate it. I programmed the ATMEGA for rotate the motor during 10 minutes at constant speed. The objective was to predict mathematically how many revolutions and what will be the end position of the shaft after that time. Then I would compare this theorical results with the reality.

Unfortunately the tests results were unsatisfactory. At every test i made, it seemed that the engine was loosing steps, accumulating errors in each revolution.

I tried to optimize the code, without success, so I began to read internet documents about the clock accuracy of Arduino UNO, the registers used for "counting the time", the delay(), delaymicros(), millis () and micros() functions... They were very hard readings and tests, without good results. My goal was to use the internal oscillator of 8Mhz so I opted to look for another solution to this problem. I found the solution using the microcontroller interruptions.

With interruptions the motor control was broadly simplified. The idea was to avoid control the time delays programatically and waste time on loops or arithmetic and logic operations. All of this instrucctions maintain the microcontroller busy and could introduce delays when sending the motor signals. Interruptions allow you to instruct the CPU to run a very simple routine every X milliseconds. This routine is used to advance a step. The interrupts are executed with priority, no matter what the CPU is doing, this way each step occurs at its right moment, without delays.

For someone like me, with no experience in ATMEGA specifications, this seemed low-level programming, too many aspects closely related to micropocesador and clock signal. So I finally looked for a library to help me, "TimerOne".

The piece of code that initializes the interrupts establish the time period between interrupts activation (AstroDelay). In each activation the associated function (stepMotor) is executed, associated with the interrupt on its initialization using attachInterrupt(). To stop and start the motor, the interrupt is enabled and disabled by calling the start() and stop() procedure. Once initialized, a new time period can be set with setPeriod().


Timer1.initialize(AstroDelay); Timer1.attachInterrupt(stepMotor); Timer1.stop(); ... Timer1.setPeriod(period / (half_step + 1)); Timer1.start();

With the engine controlled in this way, the code that runs on each interrupt is very simple. First we define the vectors that store the activation order of the motor coils, both in full-step and half-step mode:


#define motor_number_of_steps 48 //step per revolution //secuence: vector for motor movements //Array [0] -> Full-step 4 steps //Array [1] -> Half-step 8 steps const bool secuence[2][8][4] = { {{1,1,0,0},{0,1,1,0},{0,0,1,1},{1,0,0,1},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}}, {{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,1,0},{0,0,1,1},{0,0,0,1},{1,0,0,1}} };
Bipolar paso completo
Bipolar medio paso

Finally the interrupt associated function that moves one step the motor shaft:


void stepMotor(void) { // Se ejecuta en cada interrupción int step = stepCount % steps_secuence; digitalWrite(motor_pin_1, secuence[half_step][step][0]); digitalWrite(motor_pin_2, secuence[half_step][step][1]); digitalWrite(motor_pin_3, secuence[half_step][step][2]); digitalWrite(motor_pin_4, secuence[half_step][step][3]); stepCount = (stepCount + direction) % number_of_steps_per_revolution; }

The operation of this loop can be explained graphically. The color sectors represent the stepCount variable, while the gray-scale sectors represent the step variable.

With 48 steps per revolution of our motor (number_of_steps_per_revolution), at each step we will use one of the 4 (step_sequence) different coils combinations, 8 if we use half-step.

After 48 steps (96 using half-step) the motor has done a full revolution and we have iterated 12 times over the coils combinations array.

Two additional functions are used for the motor control: start_motor() and stop_motor().

With start_motor() we can do rotate the motor shaft choosing a period time between steeps, a rotation method (full or half-step) and, opcionally, the movement direcction (up o down).


void start_motor(long period, int half_mode) { if (half_mode == 1) { half_step = 1; number_of_steps_per_revolution = motor_number_of_steps * 2; steps_secuence = 8; } else { half_step = 0; number_of_steps_per_revolution = motor_number_of_steps; steps_secuence = 4; }
Timer1.stop(); Timer1.setPeriod(period / (half_step + 1)); Timer1.start(); running = true;}

With stop_motor() we can stop the motor, thus all the motor lines remains at stand-by mode. We also switch-off the motor activity led.


void stop_motor() { Timer1.stop(); digitalWrite(motor_pin_1, 0); digitalWrite(motor_pin_2, 0); digitalWrite(motor_pin_3, 0); digitalWrite(motor_pin_4, 0); running = false; led = false; digitalWrite(running_led_pin, led);}

Sensors

Las barreras iR se comprueban cada segundo cuando el motor esta en marcha. Sólo se comprueba el sensor correspondiente al sentido de la marcha.

    • The lower sensor (sensor_up) acts with the engine in the upward direction, normally the beam is broken (low level) and is an error if iactivated (high level).
    • The upper sensor (sensor_down) acts with the engine in the down direction, normally the beam isn't broken (high level) and is an error if ideactivated (low level).

In order to find minimun value that activates the down side sensor we have to do some test readings. We have increased the distance between emitter and receiver, so the minimun/maximun at high state are in 871-1017 range. This narrow range forces us to set an average border value of 950 (ir_up_threshold).


The upper sensor has normal operation and its high/low values are between 31 and 1017. In this case you can put a border value of 100 (ir_down_threshold).

When an error state is detected in a sensor, the motor is stopped and a high frequency sound is emitted for the upper sensor, and a low frequency sound for the lower one.

In case of malfunctioning sensors, due to failure or too much light pollution, their verification can be deactivated by means of the variable (do_check_sensors).


#define sensor_down A1 //Rango sensor medido (low-high): 871-1017 running up#define sensor_up A0 //Rango sensor medido (low-high): 37-1017 running down#define ir_up_threshold 950 // con 900 hay falsos positivos#define ir_down_threshold 100boolean check_sensors_actived(unsigned short int dir) { if (do_check_sensors) { if (dir == down) { //down direction if (analogRead(sensor_down) > ir_down_threshold) { //End stop running down stop_motor(); playBuzz(buzz_down); return true; } } else { //up direction if (analogRead(sensor_up) < ir_up_threshold) { //End stop running up stop_motor(); playBuzz(buzz_up); return true; } } } return false;}

Battery

The battery status is read by measuring the voltage present at the output of the voltage divider. This way we ensure that digital input isn't feed at higher voltages than 5v.

The show_battery_status() function reads such voltage value and applies a scale transformation to display the actual value present at voltage divider input. The circuit is designed so that the maximum input value of 21.67v is equivalent to 5v at the output, also we take into account, for calculating battery status, the voltage drop (0.76v) caused by D1 diode, that protects the power supply of polarity reversals.

To show the battery status is used the bi-color led LD1, which indicates the system power status with the following color scheme:

    • Green: The system is on and the battery has a voltage greater than or equal to 11v.
    • Orange: The system is on and the battery has a voltage greater than or equal to 10v and less than 11v.
    • Red: The system is on and the battery voltage is less than 10v.
    • Off: The system is power down.

Finally we store the current time in the last_bat_check variable, since the battery is only measured every 20secs.


void show_battery_status() { float value; int sensorValue;
// Basado en un divisor de voltaje: // // bat_sensor_pin // Ra Vout Rb // Vin ●──████──────┴──────███──● Gnd // Vout=(Rb/(Ra+Rb))*Vin // // Ra = 30K Ω Rb = 9K Ω // Voltajes min..max // Vin <-> Gnd --> 0..21.667 volts // Vout <-> Gnd --> 0..5 volts
// cambio de escala --> (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min // cambiar escala a 0.0 - 25.0 --> (sensorValue - 0) * (21.667 - 0.0) / (1023 - 0) + 0.0
sensorValue = analogRead(bat_sensor_pin); // diodo de entrada tiene una caida de 0.76v value = (sensorValue * 21.667 / 1023) + 0.76;
if (value >= 11) { digitalWrite(power_led_pin, true ); digitalWrite(battery_led_pin, false ); } if (value < 11 && value >= 10) { digitalWrite(power_led_pin, true ); digitalWrite(battery_led_pin, true ); } if (value < 10) { digitalWrite(power_led_pin, false ); digitalWrite(battery_led_pin, true ); } last_bat_check = millis();}

Interface

The user interface is based on two leds, a buzzer and a pushbutton. The LD1 led indicates that the system is power on and the battery status. The led LED1 indicates activity on the motor and blinks red when the engine is running, every 1sec when the sensors are active and every 2sec when deactivated.

The mount functions are controlled by pressing the S1 pushbutton. With short presses you can control the different options:

    1. With one press the motor starts up if it is stopped and stops if it is running.
    2. Two short presses change the full-step motor mode to half-step and vice versa. This allows smoother operation and control the motor torque.
    3. Three pulsations cause quick closing of the mount plates to their initial position. Remember that if the sensors have been deactivated, the plates may collide and overheat the motor.
    4. Four presses and the plates open at fast speed. Remember that if the sensors have been disabled the screw can exit the nut, opening the plates abruptly.
    5. Five presses disables or activates the sensors. You can check the status of this option by observing the flashing latency of the LED1 led.

With each option selected the system confirms it by beeping as many times as the number of the selected option.

The function used to play sounds is playBuzz(). It allows to play a long or several short beeps by selecting the frequency of the sound. The duration of the sounds is fixed in the code.


void playBuzz(unsigned short int frec, unsigned short int rep = 0) { unsigned short int dur = 50, pause = 0; if (rep == 0) { dur = 2000; rep = 1; } else pause = 30; for (unsigned short int i = 0; i < rep; i++) { noTone(buzzer_pin); tone(buzzer_pin, frec, dur); delay(dur); noTone(buzzer_pin); delay(pause); }}

In the program's loop() function, the sensor, battery and button status are checked. A ClickButton library is used to detect button presses.


Preparing the ATmega chip

Once the code was finished and tested on the Arduino ONE, I prepared the ATmega to receive the code. First I had to configure it to work with the 8Mhz internal clock as I bought it with a bootloader configured for an external 16Mhz clock. The procedure for programming the chip using an external Arduino is described here.

Using an Arduino to record the bootloader in a ATmega configured to 8Mhz
Using an Arduino to record the bootloader in a ATmega configured to 8Mhz
Using an Arduino to record the bootloader in a ATmega configured to 16Mhz
Using an Arduino to record the bootloader in a ATmega configured to 16Mhz
The Designed circuit makes it easy to connect to the ISP using the on-board programming pins
The designed circuit makes it easy to connect to the ISP using the on-board programming pins

The procedure for recording the bootloader needs to use an Arduino as an ISP (In-System Program) using the sketch "ArduinoISP.ino", that comes with the Arduino IDE, and record the bootloader with the menu options.

I used Optiboot instead of the zip file described in the section "Minimal Circuit (eliminating the External Clock)". Optiboot is more optimized and, following the installation instructions of the file README.md, makes easy the correct selection of the bootloader, this is "Optiboot on 28-pin CPUs-ATmega328p-8MHz (int)". If the bootloader writing fails or the Arduino IDE informs that it doesn't recognize the signature of the target chip it is possible that the ATmega is configured to use an external clock, thus it will be necessary to use the same circuit with the addition of an external 16Mhz oscillator and remove it once ATmega has been set up for the internal 8Mhz clock.

Arduino IDE board selection

The same circuit is used to load the program. Whenever the ISP circuit is used to program the ATmega, it is necessary to remember to unplug the battery, so that the Atmega is fed from the Arduino board.

In the case of an error recording the ATmega, by mistake in the connection (for example powered the ATmega in the line of 3.3v), one can try to recover the chip using an high voltage programmer like the one here. With this we can rewrite the correct configuration bits (fuses) and rewrite the bootloader and the program. The fuses are 3 bytes of EEPROM memory that remains between power cycles, but that can be changed as many times as you want. The fuses determine how the chip will act, if it has a bootloader, at what speed and voltage it is going to work, etc. To explore the meaning of these configuration bits you can use a "fuse calculator".

At this point the mount is ready for operation!

The full code

// <Star Tracker Barn Door. A software to control a BarnDoor tracker.>// Copyright (C) <2018> <@emvilza>//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <TimerOne.h>#include <ClickButton.h>
#define buzz_up 262#define buzz_down 4186#define buzz_pulse 65#define up -1#define down 1#define AstroDelay 210071.0952L#define RewindDelay 5000L
#define bat_sensor_pin A2#define sensor_down A1 //Rango sensor medido (low-high): 871-1017 running up#define sensor_up A0 //Rango sensor medido (low-high): 37-1017 running down#define ir_up_threshold 950 // con 900 hay falsos positivos#define ir_down_threshold 100
#define running_led_pin 3#define power_led_pin 2#define battery_led_pin 1#define buzzer_pin 9#define boton_pin 4#define motor_pin_1 5 // vo5 vo1am am1ne ne10 motor_pin_1 naranja d5-uln1-c10-m4 naranja#define motor_pin_2 7 // az7 az5n n5vc vc6 motor_pin_2 amarillo d7-uln5-c6-m3 amarillo#define motor_pin_3 6 // vc6 vc3v v3am am8 motor_pin_3 marron d6-uln3-c8-m2 marron #define motor_pin_4 8 // na8 na7b b7na na4 motor_pin_4 negro d8-uln7-c4-m1 negro
#define motor_number_of_steps 48 //step per revolution
//secuence: vector for motor movements//Array [0] -> Full-step 4 steps//Array [1] -> Half-step 8 stepsconst bool secuence[2][8][4] = { {{1,1,0,0},{0,1,1,0},{0,0,1,1},{1,0,0,1},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}}, {{1,0,0,0},{1,1,0,0},{0,1,0,0},{0,1,1,0},{0,0,1,0},{0,0,1,1},{0,0,0,1},{1,0,0,1}}};
// use volatile for interrupt shared variablesvolatile unsigned short int stepCount = 0;volatile unsigned short int direction = up;volatile unsigned short int steps_secuence; //steps secuence segun half stepvolatile unsigned short int number_of_steps_per_revolution; //steps per revolution segun half_stepvolatile unsigned short int half_step = 1; // 0 full-step 1-> half-stepunsigned short int half_step_copy = half_step;volatile boolean running = false;volatile boolean led = true;
boolean do_check_sensors = true;unsigned short int do_check_sensors_period = 0;
ClickButton boton(boton_pin, LOW, CLICKBTN_PULLUP);unsigned long last_bat_check = 0, last_sensor_check = 0;
void setup(void) { pinMode(motor_pin_1, OUTPUT); pinMode(motor_pin_2, OUTPUT); pinMode(motor_pin_3, OUTPUT); pinMode(motor_pin_4, OUTPUT); //pinMode(sensor_up, INPUT_PULLUP); //disable up sensor Timer1.initialize(AstroDelay); Timer1.attachInterrupt(stepMotor); Timer1.stop();
// Setup button timers (all in milliseconds / ms) // (These are default if not set, but changeable for convenience) boton.debounceTime = 20; // Debounce timer in ms boton.multiclickTime = 500; // Time limit for multi clicks boton.longClickTime = 1000; // time until "held-down clicks" register
pinMode(running_led_pin, OUTPUT); pinMode(battery_led_pin, OUTPUT); pinMode(power_led_pin, OUTPUT); pinMode(buzzer_pin, OUTPUT); show_battery_status();}
void start_motor(long period, int half_mode, int dir) { direction = dir; start_motor(period, half_mode);}
void start_motor(long period, int half_mode) { if (half_mode == 1) { half_step = 1; number_of_steps_per_revolution = motor_number_of_steps * 2; steps_secuence = 8; } else { half_step = 0; number_of_steps_per_revolution = motor_number_of_steps; steps_secuence = 4; }
Timer1.stop(); Timer1.setPeriod(period / (half_step + 1)); Timer1.start(); running = true;}
void stop_motor() { Timer1.stop(); digitalWrite(motor_pin_1, 0); digitalWrite(motor_pin_2, 0); digitalWrite(motor_pin_3, 0); digitalWrite(motor_pin_4, 0); running = false; led = false; digitalWrite(running_led_pin, led);}
void stepMotor(void) { int step = stepCount % steps_secuence; digitalWrite(motor_pin_1, secuence[half_step][step][0]); digitalWrite(motor_pin_2, secuence[half_step][step][1]); digitalWrite(motor_pin_3, secuence[half_step][step][2]); digitalWrite(motor_pin_4, secuence[half_step][step][3]); stepCount = (stepCount + direction) % number_of_steps_per_revolution;}
void show_battery_status() { float value; int sensorValue;
// Basado en un divisor de voltaje: // // bat_sensor_pin // Ra Vout Rb // Vin ●──████──────┴──────███──● Gnd // Vout=(Rb/(Ra+Rb))*Vin // // Ra = 30K Ω Rb = 9K Ω // Voltajes min..max // Vin <-> Gnd --> 0..21.667 volts // Vout <-> Gnd --> 0..5 volts
// cambio de escala --> (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min // cambiar escala a 0.0 - 25.0 --> (sensorValue - 0) * (21.667 - 0.0) / (1023 - 0) + 0.0
sensorValue = analogRead(bat_sensor_pin); // diodo de entrada tiene una caida de 0.76v value = (sensorValue * 21.667 / 1023) + 0.76;
if (value >= 11) { digitalWrite(power_led_pin, true ); digitalWrite(battery_led_pin, false ); } if (value < 11 && value >= 10) { digitalWrite(power_led_pin, true ); digitalWrite(battery_led_pin, true ); } if (value < 10) { digitalWrite(power_led_pin, false ); digitalWrite(battery_led_pin, true ); } last_bat_check = millis();}
void playBuzz(unsigned short int frec, unsigned short int rep = 0) { unsigned short int dur = 50, pause = 0; if (rep == 0) { dur = 2000; rep = 1; } else pause = 30; for (unsigned short int i = 0; i < rep; i++) { noTone(buzzer_pin); tone(buzzer_pin, frec, dur); delay(dur); noTone(buzzer_pin); delay(pause); }}
boolean check_sensors_actived(unsigned short int dir) { if (do_check_sensors) { if (dir == down) { //down direction if (analogRead(sensor_down) > ir_down_threshold) { //End stop running down stop_motor(); playBuzz(buzz_down); return true; } } else { //up direction if (analogRead(sensor_up) < ir_up_threshold) { //End stop running up stop_motor(); playBuzz(buzz_up); return true; } } } return false;}
void loop(void) { unsigned long now;
now = millis();
//check the battery every 20s if ((now - last_bat_check) > 20000) { show_battery_status(); }
//activity led & check sensors every 2s (sensor disabled) or 1s (sensor enabled) // Led activity period: ((0|1)+1)*1000 ms ===> 2000ms check sensors & 1000ms no check sensors if (running && (now - last_sensor_check > (do_check_sensors_period + 1) * 1000)) { last_sensor_check = now; led = !led; digitalWrite(running_led_pin, led ); //toggle led running-sensor check_sensors_actived(direction); }
// Update button state boton.Update();
// 1 click => START/STOP motor in astronomical mode if (boton.clicks == 1) { playBuzz(buzz_pulse, 1); if (running) { stop_motor(); } else { if (check_sensors_actived(up) == false) start_motor(AstroDelay, half_step_copy, up); } }
// 2 clicks = Half step/Full step operation change mode during astronomical mode if (boton.clicks == 2) { playBuzz(buzz_pulse, 2); half_step_copy = (half_step_copy + 1) % 2; if (check_sensors_actived(up) == false) start_motor(AstroDelay, half_step_copy, up); }
// 3 clicks = Fast Rewind of plates if (boton.clicks == 3) { playBuzz(buzz_pulse, 3); if (check_sensors_actived(down) == false) { start_motor(RewindDelay, 0, down); } }
// 4 clicks = Fast Forward of plates if (boton.clicks == 4) { playBuzz(buzz_pulse, 4); if (check_sensors_actived(up) == false) { start_motor(RewindDelay, 0, up); } }
// 5 clicks = Toggle (de)activate check sensors if (boton.clicks == 5) { playBuzz(buzz_pulse, 5); do_check_sensors = !do_check_sensors; do_check_sensors_period = (do_check_sensors_period + 1) % 2; }}