Gimbalino
Introducción
Uno de los usos más destacados de los Dispositivos Empotrados es en los Estabilizadores, también conocidos como Gimbals. Estos dispositivos son esenciales para la estabilización de cámaras en drones, teléfonos móviles y equipos de filmación profesional, permitiendo capturar imágenes y vídeos suaves y sin vibraciones. Los Gimbals utilizan sensores y motores para contrarrestar los movimientos no deseados, proporcionando una estabilidad que es crucial para la calidad del contenido visual.
En este proyecto, se desarrollará un Gimbal utilizando un Arduino Uno, tres servomotores y un sensor giroscopio + acelerómetro MPU6050. El objetivo es diseñar y construir un sistema de estabilización que pueda mantener una cámara en una posición estable, compensando los movimientos en tres ejes. La implementación del proyecto incluirá la programación del Arduino para controlar los servomotores basándose en los datos del sensor MPU6050, así como la integración de todos los componentes electrónicos y mecánicos necesarios.
Este proyecto no solo permitirá aplicar los conocimientos adquiridos en la asignatura de Diseño de Sistemas Empotrados, sino que también brindará la oportunidad de desarrollar habilidades prácticas en el diseño y construcción de sistemas de estabilización, que son altamente valoradas en la industria tecnológica actual.
Proyecto
Metodología
El proyecto se llevará a cabo en varias etapas:
– Diseño del Sistema:
· Selección de componentes: Arduino, servomotores, joystick y MPU6050 (sensor de movimiento).
· Diseño del circuito: Conexión de los componentes según el esquema eléctrico.
– Programación:
· Desarrollo del código para leer los valores del joystick y del sensor MPU6050.
· Implementación de algoritmos para controlar los servomotores en función de los valores leídos.
· Calibración del sistema para asegurar movimientos precisos y suaves.
– Construcción del prototipo:
· Montaje de los componentes en una estructura física que permita el movimiento libre de la cámara.
· Pruebas iniciales para verificar el funcionamiento del sistema.
Funciones del Gimbal
Una vez finalizado, incluirá las siguientes funciones:
– Control Manual: El usuario podrá controlar la orientación de la cámara mediante el joystick, ajustando la inclinación, la guiñada y el alabeo.
– Estabilización Automática: El sensor MPU6050 proporcionará datos de movimiento que permitirán al Gimbal estabilizar automáticamente la cámara, compensando movimientos no deseados.
– Calibración: El sistema incluirá una función de calibración para ajustar los valores iniciales del sensor y asegurar una operación precisa.
Reparto de tareas
Las tareas en las que se ha dividido este proyecto son:
· Búsqueda de materiales necesarios: El encargado de esta parte se ha hecho cargo de seleccionar los componentes que necesitaría el prototipo para ser capaz de mover un smartphone de unos 250gr de peso y obtener las medidas que se necesitan para realizar los ajustes angulares.
· Diseño de la estructura: Realizará los cálculos necesarios para contener empotrados todos los componentes del prototipo y conseguir que el movimiento de los ejes permita incorporar un teléfono con unas medidas equivalentes al tamaño medio actual.
· Elaboración del código: A partir del código encontrado, tendrá la responsabilidad de editarlo para conseguir que cumpla con los requisitos pedidos.
Materiales y coste
Componentes del Proyecto
Para la construcción del Estabilizador, hemos empleado los siguientes elementos:
- Servomotores : Responsables de controlar los movimientos en los ejes de rotación (pitch, yaw y roll), proporcionando estabilidad y precisión al dispositivo.
- Placa protoboard: Utilizada para interconectar los distintos componentes electrónicos de forma flexible durante la fase de desarrollo.
- Interruptor: Permite controlar el encendido y apagado del sistema de manera manual.
- Arduino Uno: Actúa como el controlador principal, procesando las señales del joystick y enviando las instrucciones a los motores.
- Joystick: Proporciona una interfaz intuitiva para el control manual del gimbal, permitiendo dirigir su orientación.
- Cables: Conectan y suministran energía y señales entre los distintos componentes.
- Batería externa: Fuente de alimentación autónoma que garantiza el funcionamiento del sistema.
La tabla se detalla a continuación:
MATERIALES | UNIDADES | PRECIO/ UNIDAD | PRECIO TOTAL |
TUERCAS M3 | 25 | 0,07 € | 1,74 € |
SERVO MG996R-180º | 3 | 5,99 € | 17,97 € |
KIT (PLACA PROTOBOARD, JOYSTICK, CABLES Y ARDUINO UNO) | 1 | 39 € | 39 € |
BATERÍA 7.4 V | 1 | 9,59 € | 9,59 € |
TORNILLOS M3 | 20 | 0,06 € | 1,25 € |
FILAMENTO PLA IMPRESIÓN 3D | 300g | 15 € /1 KG | 4,5 € |
INTERRUPTOR | 1 | 0,75 € | 0,75 € |
MÓDULO MPU6050 | 1 | 2,95 € | 2,95 € |
PRECIO TOTAL | 77,75 € |
Implementación
En el prototipo final hicimos un diseño 3D con sus distintas partes, formadas por:
Un cilindro para el mango, en cuyo interior se encuentra uno de los servos, la breadboard con las conexiones de todos los elementos y la batería. En su exterior se encuentra el joystick y el sensor MPU.
Sus medidas son de 22 cm de alto, con un grosor de 0,4 cm y un radio de 3,9cm, una de las caras está vacía y en la otra se encuentra tres agujeros: un círculo de 0,5 cm de radio donde pasaran los cables de los servos, un rectángulo en el medio, donde estará uno de los motores, con 2 cm de ancho y 4 cm de largo y otro rectángulo, donde pasarían los cables del giroscopio y joystick, con 0,5 cm de ancho y 1,5 cm de largo.
La tapa del cilindro que se encargará de cerrar el mango tiene como medidas un primer círculo de 3,9 cm de radio y una altura de 0,4 cm y otro circulo interior hueco con 3,5 cm de radio y una altura de 2 cm, con un grosor de 0,2 cm.
Una articulación en forma de L, que contendrá al servo encargado de la rotación en el eje Y, cuya parte más larga medirá 7,5 cm de largo dividido en un rectángulo de 5,5 cm y una semicircunferencia de 2 cm de radio. A una distancia de 2 cm desde el borde curvo y 1 cm desde el lateral se tiene un recorte con forma rectangular de 2 cm de ancho y 4 cm de largo, en el cual se alojará el servo encargado del giro torno al eje X.
La otra cara tiene 4 cm de alto, compuesta por una semicircunferencia de 2 cm de radio y un rectángulo de 2 cm de alto, todo ello con una anchura de 4 cm y un grosor de 0,4 cm.
La última articulación tiene una parte central que mide 8cm de largo, y dos ramas inclinadas un ángulo de 135° cada una respecto a la base, ambas tienen el mismo largo, 7,5 cm y se componen de un rectángulo de 5,5 cm de largo y una semicircunferencia de 2 cm de radio. A una distancia de 2 cm desde el borde de la semicircunferencia y a 1 cm desde el lateral se encuentra el hueco de forma rectangular con 2 cm de ancho y 4 cm de largo, donde se situará el último motor.
El esquema de conexión seguido es el siguiente:
El joystick tiene conectado el VRX al pin A1 del Arduino, el cual aporta los movimientos en el eje x, y el VRY está conectado al pin A2 del Arduino, el cual aporta los movimientos en el eje y. Los pines GND y VCC estarán conectados a tierra y carga respectivamente.
Los tres motores están conectados a los pines 11, 10 y 9 del Arduino, a los cuales dará la información para su movimiento. El servomotor conectado al pin 11 moverá el eje Z, el de la posición 10 moverá en el eje Y y en la poción 9 moverá en el eje X.
El sensor MPU tiene conectado el pin SCL al pin A5 del Arduino, el SDA al pin A4 y el INT en el pin digital 2.
Código
Además se ha empleado el código generado por Dejan en la web HowToMechatronics que también puede encontrarse en la Bibliografía, añadiendo el control con joystick y el mantenimiento de la posición de los ejes en la posición requerida. Este código inicialmente reportaba problemas que serán comentados en el apartado dedicado a ello y que han llevado a añadir modificaciones extra en las comprobaciones que realiza el código.
Declaración de variables:
En un primer lugar se declaran los 3 servomotores según el eje al que corresponden. La variable “correcto” se utiliza para almacenar el valor correcto de ángulo rotado del eje Z, que será calculado posteriormente en un proceso de calibración consistente en un bucle que realiza 300 medidas y se queda con la última proporcionada por el sensor.
Se define un umbral utilizado en la gestión del joystick, para generar una zona alrededor del punto neutro (no se acciona) en la que no se realice acción alguna para evitar desplazamientos indeseados.
A continuación, disponemos de las variables para la gestión del MPU donde se almacena si el procesador dmp que incluye está correctamente conectado y habilitado, las interrupciones devueltas por el sensor cuando tiene valores almacenados en el buffer de transmisión de datos, el estado del dispositivo tras cada acción realizada, el tamaño del paquete de datos, un contador para saber en todo momento la cantidad de bytes que se tiene actualmente en el buffer y el buffer de transmisión de datos entre el sensor y el Arduino.
También se incluyen las variables que almacenan los datos medidos por el MPU como los cuaterniones, que son una forma matemática de representar rotaciones en el espacio tridimensional y presentan menos problemas que los ángulos de Euler, los cuales también se obtienen de este sensor para conseguir una mayor precisión en la medida y cálculo de loa grados inclinados.
Se tienen variables para almacenar las aceleraciones, que serán importantes para el cálculo del giro alrededor del eje Z y para almacenar el tiempo que transcurre en un movimiento, para realizar dicho cálculo.
Se termina con las variables del joystick, como sus pines de conexión, los valores devueltos para la posición neutra cuando está alimentado a la corriente de 7,4v aportada por la batería, pues a 5v, estos deberían ser de 512 ya que el joystick tiene un rango de valores de 0 a 1023, sin embargo, por la forma de conexión establecida, los valores se ven alterados.
Y las variables de los servomotores como sus pines de conexión, el establecimiento del paso de ángulo que cada motor debe realizar cuando se acciona el joystick en algún sentido y los offsets de los ejes para mantener el ángulo en el que deben permanecer estabilizados los 3 ejes tras haberlo establecido con el joystick.
Es necesario indicar los valores iniciales del offset para los valores leídos por el giroscopio, que en cada caso serán diferentes, según cómo esté fabricado, en este caso los valores empleados son los que se muestran en la imagen anterior.
Se sigue comprobando que la inicialización del dmp ha sido correcta comprobando el contenido de la variable “estadoDev”. En caso de no haber problema se procede con su activación y se espera a la primera interrupción, que indicará la existencia de nuevos datos, si al contrario sucede algún problema, se imprime el mensaje de error.
Termina la configuración conectando cada servomotor al pin que le corresponde.
Si el estado es el adecuado, se espera hasta tener datos disponibles en el buffer, implementado con el bucle while que aparece y en que se comprueba que se recibe una interrupción desde el sensor, la cual indicará la existencia de datos y permitirá salir del bucle y continuar ejecutando.
Se devuelve el estado de la interrupción a false, se lee estado actual del mpu y se recibe el número de bytes contenidos en el buffer. Esta información es importante para las comprobaciones que se realizan a continuación, empezando por el estado en que se encuentra el mpu, que deberá estar activo en ambos casos iniciales.
En el primer caso se comprueba si se produce Overflow de los datos del buffer y si se ha producido, se resetea el buffer por completo para volver a obtener datos nuevos y poder continuar con la ejecución del programa sin interrumpir el funcionamiento, aunque en el resultado final se produces micro cortes en el movimiento del Estabilizador. Se devuelve el mensaje de error correspondiente a través del monitor serie.
Para la siguiente condición se establece que, si el estado de la interrupción devuelta por el dmp es activo, es decir, contiene datos útiles, se espera a que se devuelvan los datos producidos y se actualiza el valor de la variable contador de bytes actualmente en el buffer.
A continuación, se procede a obtener los datos de cuaternión, gravedad y ángulo de inclinación en los 3 ejes desde el dmp y almacenarlos en las variables dedicadas a estos.
Se convierten los radianes de los ángulos a grados sexagesimales para poder ser procesados por los servomotores y se inicia el proceso de calibrado del valor del eje Z, el cual consiste en una toma de 300 medidas hasta quedarse con la última como la correcta.
Tras esto, se utiliza este valor para corregir la desviación del valor leído por el eje Z haciendo la resta del valor leído con el valor “correcto” y almacenando la diferencia en ypr[0].
El siguiente elemento por ejecutar, es el cálculo del ángulo girado en el eje Z, el cual, se realiza a partir de los valores obtenidos y el tiempo transcurrido desde el instante en que se inicia el movimiento hasta que se desplaza el servomotor. Es necesario convertir este tiempo en segundos para poder mantener el sentido de las magnitudes que utiliza el giroscopio para los valores de aceleración correspondientes.
Importante recalcar que se ha establecido un factor de 0,05 para reducir la velocidad de movimiento de este eje, pues en un principio el ángulo recorrido por este era mucho mayor que el que se quiere compensar haciendo que el Estabilizador deje de apuntar a la posición deseada.
En la línea 208 se lee los valores del joystick para sus dos ejes y posteriormente se comprueba si existe señal de movimiento, es decir, si se ha desplazado, y en caso afirmativo, se actualizan los valores de los offsets correspondientes, aumentando su valor un paso cada vez, que corresponde a un ángulo de un grado en la vida real.
A partir de este punto, ya se tienen calculados los ángulos de cada eje, sin embargo, estos no están adaptados al rango de movimiento que ofrecen los servomotores empleados. Para solucionar esto, se utiliza el método constrain para mantener los valores de los offsets en un rango de -90 a 90 grados, que es lo admitido por los servos, y la función map encargada de transformar los datos finales antes de lanzarlos a los servos para que se encuentren en el rango de 0 a 180 grados.
Debe recalcarse en este punto que el producto final debe compensar los movimientos de los 3 ejes, haciendo movimientos en el sentido contrario a estos y para ello se tiene en dichas funciones map el rango establecido al revés par el eje Z (de 180 a 0, en lugar de 0 a 180). Se termina escribiendo estos datos en los servos.
Problemas y soluciones encontradas
Otro de los problemas que tuvimos fue con el sensor, pues el primero comprado no contaba con giroscopio, solo contaba con acelerómetro y esto producía que los movimientos fuesen menos precisos, la solución a este problema ha sido la implementación de un nuevo MCP6050, concretamente el modelo GY-521 que sí incluye tanto giroscopio como acelerómetro.
El mayor reto de este proyecto ha sido sin duda el código, pues a pesar de partir de una base ya existente, ha resultado muy complicado conseguir alcanzar el movimiento deseado.
En un principio, el prototipo si quiera se movía, y al comprobar los valores devueltos en el monitor serie no se obtenía información que pudiera indicar un posible fallo de conexión con el dmp o algún otro fallo, así que se procedió a colocar prints estratégicos en distintos puntos del programa para comprobar hasta qué punto se llegaba a ejecutar, llegando a la conclusión de que se quedaba parado a la espera de datos en el bucle while del inicio del loop.
Esto se debe probablemente a un sensor defectuoso, pues este bucle debe retener el programa hasta que se generen datos en el sensor y permitir la ejecución en cuanto se tengan estos, pero en este caso, las interrupciones nunca llegaban, lo que indica que algo no funcionaba correctamente.
Para solucionar este error se ha decidido ignorar esta comprobación del bucle while, dejando estas líneas de código comentadas, consiguiendo que al menos se moviera por unos segundos el Estabilizador.
El siguiente error se producía a los pocos segundos de iniciar y provocaba que se quedara congelado y no reaccionara si quiera al accionamiento del joystick. Tras revisar los mensajes a través del monitor serie, se pudo comprobar que siempre saltaba el error de buffer fifo lleno impidiendo continuar con la correcta ejecución del programa. Esto se resolvió realizando un reset del buffer de comunicación cada vez que se produce el llenado y evitando que este overflow impidiera la ejecución del loop.
Bibliografía
Librerías de comunicación I2c desarrolladas por Jeff Rowberg:
https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050
Código base obtenido de:
https://howtomechatronics.com/projects/diy-arduino-gimbal-self-stabilizing-platform/
Vídeo explicativo
Autores
Álvaro Díaz Fernández
Enrique Fernández Godoy
Ángel González Rodríguez
Código completo
/*
Código basado en el ejemplo MPU6050_DMP6 de la biblioteca i2cdevlib de Jeff Rowberg:
https://github.com/jrowberg/i2cdevlib
*/
// I2Cdev y MPU6050 deben estar instalados como bibliotecas, o los archivos .cpp/.h
// para ambas clases deben estar en la ruta de inclusión de tu proyecto
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"
// La biblioteca Wire de Arduino es requerida si se usa la implementación I2CDEV_ARDUINO_WIRE
// en I2Cdev.h
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif
#include <Servo.h>
MPU6050 mpu;
//MPU6050 mpu(0x69); // <-- usar para AD0 alto
// Declaración de los 3 servos.
Servo servoZ;
Servo servoY;
Servo servoX;
float correcto;
int j = 0; //Contador para la calibración al inicio.
#define UMBRAL 100 // Zona sin movimiento del joystick, para asegurar que no se mueva
//si el jostick no se mueve.
#define SALIDA_LEIBLE_YAWPITCHROLL
#define PIN_INTERRUPCION 2 // usar el pin 2 en Arduino Uno y la mayoría de las placas
bool estadoParpadeo = false;
// Variables de control/estado de MPU
bool dmpListo = false; // establecer en verdadero si la inicialización de DMP fue exitosa
uint8_t estadoIntMpu; // contiene el byte de estado de interrupción actual del MPU
uint8_t estadoDev; // estado de retorno después de cada operación del dispositivo (0 = éxito, !0 = error)
uint16_t tamanioPaquete;// tamaño esperado del paquete DMP (el valor por defecto es 42 bytes)
uint16_t contadorFifo; // conteo de todos los bytes actualmente en FIFO
uint8_t bufferFifo[64]; // buffer de almacenamiento FIFO
// Variables de orientación/movimiento
Quaternion q; // [w, x, y, z] contenedor de cuaterniones
VectorInt16 aa; // [x, y, z] mediciones del sensor de aceleración
VectorInt16 aaReal; // [x, y, z] mediciones del sensor de aceleración sin gravedad
VectorInt16 aaMundo; // [x, y, z] mediciones del sensor de aceleración en el marco del mundo
VectorFloat gravedad; // [x, y, z] vector de gravedad
float euler[3]; // [psi, theta, phi] contenedor de ángulos de Euler
float ypr[3]; // [yaw, pitch, roll] contenedor de yaw/pitch/roll y vector de gravedad
float anguloZ = 0; // Ángulo girado alrededor del eje Z
unsigned long tiempoAnterior = 0; // Tiempo anterior para cálculo de delta
// Variables para el joystick
const int PIN_JOYSTICK_X = A0; // Pin eje X del joystick
const int PIN_JOYSTICK_Y = A1; // Pin eje Y del joystick
const int NEUTRAL_X = 830; // Valor neutro de eje X
const int NEUTRAL_Y = 815; // Valor neutro del eje Y
const int PASO_OFFSET = 1; // Paso para el servo
int offsetServoZ = 0; // Offset servo Z
int offsetServoX = 0; // Offset servo X
// ================================================================
// === RUTINA DE DETECCIÓN DE INTERRUPCIONES ===
// ================================================================
volatile bool interrupcionMpu = false; // Indica si se produce una interrupcion por parte del mpu.
void dmpDatosListos()
{
interrupcionMpu = true;
}
// ================================================================
// === CONFIGURACIÓN INICIAL ===
// ================================================================
void setup() {
// unirse al bus I2C (la biblioteca I2Cdev no hace esto automáticamente)
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
Wire.begin();
Wire.setClock(400000); // 400kHz reloj I2C. Comenta esta línea si tienes dificultades de compilación
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
Fastwire::setup(400, true);
#endif
// inicializar la comunicación serie
Serial.begin(38400);
while (!Serial); // esperar a la enumeración de Leonardo, otros continúan inmediatamente
// inicializar el dispositivo
Serial.println(F("Inicializando dispositivos I2C..."));
mpu.initialize();
pinMode(PIN_INTERRUPCION, INPUT);
estadoDev = mpu.dmpInitialize();
// proporciona tus propios offsets de giroscopio aquí, escalados para la sensibilidad mínima
mpu.setXGyroOffset(17);
mpu.setYGyroOffset(-69);
mpu.setZGyroOffset(27);
mpu.setZAccelOffset(1551);
// asegúrate de que funcionó (devuelve 0 si es así)
if (estadoDev == 0) {
// activar el DMP, ahora que está listo
Serial.println(F("Activando DMP..."));
mpu.setDMPEnabled(true);
attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPCION), dmpDatosListos, RISING);
estadoIntMpu = mpu.getIntStatus();
// establecer nuestra bandera DMP Listo para que la función principal loop() sepa que está bien usarlo
Serial.println(F("DMP listo! Esperando la primera interrupción..."));
dmpListo = true;
// obtener el tamaño esperado del paquete DMP para comparación posterior
tamanioPaquete = mpu.dmpGetFIFOPacketSize();
} else {
// ¡ERROR!
// 1 = carga de memoria inicial fallida
// 2 = actualizaciones de configuración de DMP fallidas
// (si va a fallar, generalmente el código será 1)
Serial.print(F("La inicialización de DMP falló (código "));
Serial.print(estadoDev);
Serial.println(F(")"));
}
// Conexión de los servos.
servoZ.attach(11);
servoY.attach(10);
servoX.attach(9);
}
// ================================================================
// === BUCLE PRINCIPAL DEL PROGRAMA ===
// ================================================================
void loop() {
// si la programación falló, no intentes hacer nada
if (!dmpListo) return;
// esperar a la interrupción del MPU o a paquetes adicionales disponibles
while (!interrupcionMpu && contadorFifo < tamanioPaquete) {
if (interrupcionMpu && contadorFifo < tamanioPaquete) {
// intentar salir del bucle infinito
contadorFifo = mpu.getFIFOCount();
}
}
// restablecer la bandera de interrupción y obtener el byte INT_STATUS
interrupcionMpu = false;
estadoIntMpu = mpu.getIntStatus();
// obtener el conteo actual de FIFO
contadorFifo = mpu.getFIFOCount();
// verificar desbordamiento (esto nunca debería suceder a menos que nuestro código sea demasiado ineficiente)
if ((estadoIntMpu & _BV(MPU6050_INTERRUPT_FIFO_OFLOW_BIT)) || contadorFifo >= 1024) {
// restablecer para que podamos continuar limpiamente
mpu.resetFIFO();
contadorFifo = mpu.getFIFOCount();
Serial.println(F("¡Desbordamiento de FIFO!"));
// de lo contrario, verificar la interrupción de datos DMP listos (esto debería suceder con frecuencia)
} else if (estadoIntMpu & _BV(MPU6050_INTERRUPT_DMP_INT_BIT)) {
// esperar a la longitud de datos disponibles correcta, debería ser una espera MUY corta
while (contadorFifo < tamanioPaquete) contadorFifo = mpu.getFIFOCount();
// leer un paquete de FIFO
mpu.getFIFOBytes(bufferFifo, tamanioPaquete);
// rastrear el conteo de FIFO aquí en caso de que haya más de 1 paquete disponible
// (esto nos permite leer más inmediatamente sin esperar una interrupción)
contadorFifo -= tamanioPaquete;
// Obtener valores de Yaw, Pitch y Roll
#ifdef SALIDA_LEIBLE_YAWPITCHROLL
mpu.dmpGetQuaternion(&q, bufferFifo);
mpu.dmpGetGravity(&gravedad, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravedad);
// Valores de Yaw, Pitch, Roll - Radianes a grados
ypr[0] = ypr[0] * 180 / M_PI;
ypr[1] = ypr[1] * 180 / M_PI;
ypr[2] = ypr[2] * 180 / M_PI;
// Saltar 300 lecturas (proceso de auto-calibración)
if (j <= 300) {
correcto = ypr[0]; // Yaw comienza en un valor aleatorio, así que capturamos el último valor después de 300 lecturas
j++;
}
// Después de 300 lecturas
else {
ypr[0] = ypr[0] - correcto; // Establecer el Yaw en 0 grados - restar el último valor aleatorio de Yaw del valor actual para hacer que el Yaw sea 0 grados
// Obtener la velocidad angular en el eje Z
int16_t gx, gy, gz;
mpu.getRotation(&gx, &gy, &gz);
// Calcular el ángulo de giro alrededor del eje Z
unsigned long tiempoActual = millis();
float deltaTiempo = (tiempoActual - tiempoAnterior) / 1000.0; // Convertir a segundos
anguloZ += (gz * deltaTiempo) * 0.05; // Integrar la velocidad angular para obtener el ángulo
tiempoAnterior = tiempoActual;
// Leer los valores del joystick
int joystickX = analogRead(PIN_JOYSTICK_X); // Leer el eje X
int joystickY = analogRead(PIN_JOYSTICK_Y); // Leer el eje Y
// Controlar el servo Z con el joystick en el eje X
if (joystickX < NEUTRAL_X - UMBRAL) {
offsetServoZ -= PASO_OFFSET; // Incrementar el offset
} else if (joystickX > NEUTRAL_X + UMBRAL) {
offsetServoZ += PASO_OFFSET; // Decrementar el offset
}
// Limitar el offset del servo Z
offsetServoZ = constrain(offsetServoZ, -90, 90); // Limitar el rango del offset
// Controlar el servo 1 con el joystick en el eje Y
if (joystickY < NEUTRAL_Y - UMBRAL) {
offsetServoX += PASO_OFFSET; // Incrementar el offset
} else if (joystickY > NEUTRAL_Y + UMBRAL) {
offsetServoX -= PASO_OFFSET; // Decrementar el offset
}
// Limitar el offset del servo Y
offsetServoX = constrain(offsetServoX, -90, 90); // Limitar el rango del offset
// Mapear los valores del sensor MPU6050 de -90 a 90 a valores adecuados para el control del servo de 0 a 180
int valorServoZ = map(anguloZ + offsetServoZ, -90, 90, 180, 0);
int valorServoY = map(ypr[1], -90, 90, 0, 180);
int valorServoX = map(ypr[2] + offsetServoX, -90, 90, 0, 180);
// Controlar los servos según la orientación del MPU6050
servoZ.write(valorServoZ);
servoY.write(valorServoY);
servoX.write(valorServoX);
}
#endif
}
}