Star Tracker

Programación

El código necesario para este proyecto está organizado en bloques, para el control del motor, lectura de sensores, lectura del estado de la batería y gestión de la interfaz de usuario (botón y sonidos).

Motor

Empecé la programación, en el Arduino Uno, diseñando pequeños ejemplos para el control del motor paso a paso bipolar. Al carecer de un controlador para motores de este tipo debía mandar las señales correctas a cada bobina del motor en cada momento para conseguir hacer girar el motor de una forma ordenada. Pensé en gastar librerías disponibles para ello, pero al necesitar controlar con precisión el tiempo entre paso y paso descarté su uso.

Acabé implementando el código básico para girar el motor tanto en pasos completos como en medios pasos. Con un bucle, un contador de pasos y una pausa entre cada paso, conseguí hacer girar el motor a la velocidad adecuada gracias a los cálculos realizados anteriormente.

Con este prototipo básico de programa decidí hacer una prueba para validarlo. Programé el ATMEGA para que hiciera rodar el motor durante 10 minutos a velocidad constante. El objetivo era predecir matemáticamente cuantos giros y en qué posición quedaría el motor después de ese tiempo. Después compararía los resultados con la realidad.

Desgraciadamente los resultados no fueron consistentes, por muchas pruebas que hiciera parecía que el motor perdía pasos, acumulando errores en cada vuelta.

Optimicé el código todo lo que pude, sin éxito, hasta que volviendo a internet empecé a leer sobre la precisión del reloj del Arduino UNO, los registros que usa para "contar el tiempo", los comandos delay(), delaymicros(), millis () y micros(). Fueron lecturas y pruebas muy densas, sin mucho éxito. Teniendo en cuenta que mi objetivo era utilizar el oscilador interno de 8Mhz al final opté por buscar otra solución al problema. La encontré en las interrupciones.

Con las interrupciones simplifiqué al máximo el control del motor. La idea era evitar llevar el control del tiempo por programa, evitando operaciones aritméticas, comparaciones, bucles y toma de tiempos. Todo esto ocupaba ciclos de CPU que podían introducir retrasos en el envío de las señales del motor. Las interrupciones permitían decirle a la CPU que X milisegundos ejecutara una rutina muy simple para avanzar un paso. Las interrupciones se ejecutan con prioridad, con lo que no importa en que este haciendo la CPU, de forma que cada paso se ejecuta en su instante correcto, sin demoras.

Para alguien como yo, sin experiencia en el ATMEGA, esto sonaba a programación a bajo nivel con aspectos muy relacionados con el micropocesador y señal de reloj utilizado. Por ello finalmente busqué una librería "TimerOne" que me ayudara en este aspecto.

El código que inicializa las interrupción, establece el periodo de tiempo entre activaciones de la interrupción (AstroDelay) y en cada activación se ejecuta la función asociada (stepMotor), asociada a la interrupción mediante attachInterrupt(). Entonces para parar y arrancar el motor se habilita y deshabilita la interrupción, mediante la llamada al procedimiento start() y stop(). Una vez inicializado, se puede establecer un nuevo periodo de tiempo entre activaciones con setPeriod().


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

Con el motor controlado de esta manera el código que se ejecuta en cada interrupción es muy simple. Primero la definición de vectores con el orden de activación de las bobinas para los modos de paso completo y medio paso:


#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 paso completo
Bipolar medio paso
Bipolar medio paso

Y después la función asociada a la interrupción que se encarga de realizar un paso en el motor:


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; }

El funcionamiento de este bucle se puede explicar gráficamente. Los sectores de colores representan la variable stepCount, mientras los sectores en escala de grises representan la variable step.

Diagrama de movimiento de un motor paso a paso

Teniendo en cuenta el funcionamiento del motor de 48 pasos por vuelta (number_of_steps_per_revolution), en cada paso utilizamos en bucle una de las 4 (step_sequence) combinaciones de bobinas distintas, 8 si damos medios pasos.

Tras 48 pasos (96 en medios pasos) habremos recorrido una vuelta entera y habremos hecho 12 iteraciones sobre las combinaciones de bobinas.

Dos funciones adicionales sirve para controlar el motor start_motor() y stop_motor().

Con start_motor() podemos poner en marcha el motor indicando el periodo entre pasos, el modo de rotación (completa o medio paso) y opcionalmente la dirección (subir o bajar).


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;}

Con stop_motor() detenemos el motor, dejando sus líneas en reposo. Adicionalmente apagamos el led que indica la actividad del motor.


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);}

Sensores

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.

    • El sensor inferior (sensor_up) actúa con el motor en dirección de subida, normalmente está obturado (nivel bajo) y se considera error que se active.
    • El sensor superior (sensor_down) actúa con el motor en dirección de bajada, normalmente está activado (nivel alto) y se considera error que se desactive.

Para averiguar los valores fronteras entre los dos estados (alto/bajo) del sensor inferior hay que llevar a cabo algunas lecturas de prueba. Al alterar la separación el emisor y receptor los valores bajo/alto se sitúan entre 871 y 1017. Este rango de variación tan estrecho obliga a poner un valor medio de frontera de 950 (ir_up_threshold).

El sensor superior tiene un funcionamiento normal y sus valores alto/bajo se sitúan entre 31 y 1017. En este caso se puede poner un valor de frontera de 100 (ir_down_threshold).

Cuando se detecta un estado de error en un sensor se detiene el motor y se emite un sonido agudo para el sensor superior y grave para el inferior.

En caso de mal funcionamiento de los sensores, por fallo o por exceso de contaminación lumínica, se pueden desactivar su comprobación mediante la 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;}

Batería

El estado de la batería se lee midiendo la tensión presente en la salida del divisor de voltaje. De esta forma aseguramos que no estaremos sometiendo a la entrada digital a una tensión superior a 5v.

La función show_battery_status() lee el dicho valor de voltaje y aplica un cambio de escala para mostrar el valor real a la entrada del divisor de voltaje. El circuito está pensado para que el valor máximo de entrada de 21,67v sean equivalentes a 5v en la salida, También tiene en cuenta la caída de tensión (0,76v) que provoca el diodo D1 que protege la fuente de alimentación de inversiones de polaridad.

Para mostrar el estado de la batería se utiliza el led bicolor LD1, que indica el encendido del sistema con la siguiente combinación de colores:

    • Verde: El sistema está encendido y la batería tiene un voltaje superior o igual a 11v.
    • Naranja: El sistema está encendido y la batería tiene un voltaje superior o igual a 10v y menor que 11v.
    • Rojo: El sistema está encendido y la batería tiene un voltaje menor que 10v.
    • Apagado: El sistema está apagado.

Por último almacena el instante actual en la variable last_bat_check, ya que la batería sólo se mide cada 20s.


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();}

Interfaz

La interfaz de usuario consiste en dos led, un zumbador y un pulsador. El led LD1 indica que el sistema está encendido y el estado de la batería. El led LED1 indica actividad en el motor, parpadea en rojo cuando el motor está en marcha, cada 1s cuando los sensores están activos y cada 2s cuando se desactivan.

Las funciones de la montura se controlan mediante pulsaciones en el pulsador S1. Con pulsaciones breves se pueden controlar las distintas opciones:

    1. Con una pulsación se pone en marcha el motor si está parado y se detiene si está en marcha.
    2. Dos pulsaciones breves cambia el modo del motor de paso completo a medio paso y viceversa. Esto permite un funcionamiento más suave y controlar el torque del motor.
    3. Tres pulsaciones provoca el cierre rápido de las placas de la montura a su posicion inicial. Recuerda que si se han desactivado los sensores las placas pueden chocar y sobrecalentar el motor.
    4. Cuatro pulsaciones las placas se abren a velocidad rápida. Recuerda que si se han desactivado los sensores el tornillo puede salir de la tuerca, abriendo las placas de golpe.
    5. Cinco pulsaciones desactiva o activa los sensores. Se puede comprobar el estado de esta opción observando la latencia de parpadeo del led LED1.

Con cada opción seleccionada el sistema emite una confirmación emitiendo el mismo número de pitidos que la opción seleccionada.

La función utilizada para emitir sonidos es playBuzz(). Permite emitir un sonido largo o varios cortos seleccionando la frecuencia del sonido. La duración de de los sonidos está predeterminada en el código.


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); }}

En la funcion loop() del programa se hace la comprobación de sensores, batería y el estado del botón. Para detectar las pulsaciones del botón se utiliza una librería ClickButton.


Preparación del ATmega

Una vez acabado y probado en código en el Arduino UNO preparé el ATmega para recibir el código. Primero tenía que configuralo para funcionar con el reloj interno a 8Mhz ya que lo compré con un bootloader preparado para un reloj externo de 16Mhz. El procedimiento para programar el chip mediante un Arduino externo está descrito aquí .

Usando un Arduino para grabar el cargador de arranque en un ATmega configurado a 8Mhz
Usando un Arduino para grabar el cargador de arranque en un ATmega configurado a 8Mhz
Usando un Arduino para grabar el cargador de arranque en un ATmega configurado a 16Mhz
Usando un Arduino para grabar el cargador de arranque en un ATmega configurado a 16Mhz
El circuito diseñado facilita la conexión al ISP mediante los pines de programación en placa
El circuito diseñado facilita la conexión al ISP mediante los pines de programación en placa

El procedimiento para grabar el arranque consiste en usar el Arduino como ISP (In-System Program), utilizar el sketch ArduinoISP.ino que viene con el IDE de Arduino y grabar el bootloader con las opciones del menú.

Yo utilicé Optiboot en vez del zip descrito en la sección "Minimal Circuit (Eliminating the External Clock)". Optiboot está más optimizado y siguiendo las instrucciones de instalación de su fichero README.md facilita la selección correcta del bootloader "Optiboot on 28-pin cpus - ATmega328p - 8MHz (int)". Si falla la carga del bootloader o el programa o el IDE de Arduino informa que no reconoce la firma del chip destino es posible que esté configurado para usar el reloj externo, con lo que será necesario usar el mismo circuito con la adición de un cristal oscilador externo de 16Mhz y retirarlo una vez se haya configurado para 8Mhz interno.

Menú de configuración de placa e ISP

Para cargar el programa se usa el mismo circuito. Siempre que se utilice el circuito de ISP para programar el ATmega hay que recordar desconectar la batería, para que el Atmega quede alimentado desde la placa de Arduino.

El el caso de que se produzca un error en la grabación del ATmega, por error en el conexionado (por ejemplo alimentado el ATmega en la línea de 3.3v), se puede intentar recuperar el chip usando un programador de alto voltaje como el de aquí. Con ello podemos reescribir los bits de configuración (fuses) correctos y volver a grabar el bootloader y el programa. Los fuses son 3 bytes de memoria EEPROM que permanecen sin alimentación del chip, pero que se pueden cambiar tantas veces quieras. Los fusibles determinan cómo actuará el chip, si tiene un gestor de arranque, a qué velocidad y voltaje va a funcionar, etc. Para explorar el significado de estos bits de configuración puedes usar una "Calculadora de fuses".


¡ Llegados a este punto la montura está lista para funcionar !

El código completo

// <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; }}