24 marzo 2013

Inclinometro digital con Arduino (uso de acelerómetros)


Un inclinómetro es un dispositivo que nos permite medir la inclinación en grados de una superficie. Si se han subido alguna vez a una camioneta es posible que lo hayan visto en el tablero:


En el siguiente video pueden ver rápidamente como funciona este dispositivo:



Para la entrada de ahora intentaré explicar como crear un sencillo inclinómetro utilizando un sencillo y barato acelerómetro de 3 ejes que puede ser adquirido en DealExtreme, un Arduino y un poquito de código en Python con la biblioteca PyGame. Espero les sea útil.

+Agregado:  Más información sobre los Convertidores Análogo-Digital en Arduino.

¿Qué es un acelerómetro?


Un acelerómetro es un dispositivo que es capaz de medir "aceleraciones". Por tanto, un acelerómetro electrónico es un dispositivo electrónico capaz de realizar dicha tarea.

En los últimos años gracias a compañías como Nintendo y Sony se volvieron populares este tipo de dispositivos para controlar los juegos.

Estoy más que seguro que alguna vez habrán escuchado hablar del muy popular Wiimote.

La lógica detrás de estos controles es sumamente simple, si mides las aceleraciones a las que está sometido el control, con ayuda de algunos puntos de referencia, es posible calcular la ubicación del control en el espacio.

Pero... ¿Cómo un acelerometro puede medir inclinaciones?


La respuesta nos lleva al mundo de la física: Para que exista una aceleración esta tiene que ser provocada por una fuerza. La representación de una fuerza usualmente se realiza por medio de un vector. Los vectores usualmente se dibujan como flechas:

Si el vector nos indica una dirección su "largo" nos podría indicar la magnitud de la fuerza. De esta manera si identificamos hacia a donde apunta la fuerza podemos conocer la posición del objeto sobre el cual esta fuerza se está ejerciendo.

Realmente para el inclinómetro no vamos a estar demasiado interesados en la magnitud del vector de aceleración sino más bien en su dirección. La idea es bastante simple, todos estamos bajo la influencia de la fuerza de gravedad, el acelerómetro nos permitirá conocer la dirección de esta fuerza y solo aplicaremos algunas sencillas fórmulas trigonométricas para calcular la inclinación del sensor.

El MMA7361


Buscando en DealExtreme pueden encontrar este pequeño módulo para Arduino que incluye un acelerómetro de 3 ejes. El costo es sumamente reducido cerca de $7 y es capaz de medir aceleraciones en los 3 ejes.

Este integrado tiene dos modos de funcionamiento, uno más preciso que es capaz de detectar hasta +/- 1.5G y otro con un rango un poco mayor pero menos preciso que detecta +/- 6G. Este ultimo rango supera por mucho las aceleraciones laterales que uno puede alcanzar en pista.

¿Cómo funciona? Si revisan la imágen podrán notar como hay tres pines denominados "x", "y" y "z". Estos pines mantienen un voltaje relativo a la fuerza de gravedad a la que está sometido el circuito integrado.


Entendiendo como las fuerzas G afectan el Acelerómetro


Imaginemos por un momento que tenemos nuestro circuito integrado sobre una superficie plana que se encuentra sin ninguna inclinación como muestro en el siguiente diagrama:

El acelerómetro únicamente detectará una fuerza de aceleración aplicada sobre el acelerómetro que mire la fuerza sobre Z.

Vamos a simplificar un poco las cosas y vamos a imaginar que estamos viendo el acelerómetro de lado (así solo consideramos dos dimesiones). Imaginemos que giramos el acelerómetro sobre el eje Y. Vamos a llamar a los ejes de los sensores X' y Z', entonces, las fuerzas se aplicarían de la siguiente manera:


El acelerómetro como tal no conoce en ningún momento su posición. Sin embargo conoce los componentes de la fuerza de gravedad. Podemos utilizar los valores de la fuerza que se aplica a los componentes para conocer el ángulo, utilizando una sencilla función trigonométrica inversa:


Conociendo los componentes de la fuerza G aplicados a nuestro acelerómetro podemos conocer el ángulo. Sin embargo podemos invertir el orden de los componentes dentro de la función arco-tangente para obtener el ángulo complementario. Dejo de ejercicio al lector averiguar porque esto hace que funcione como queremos.

Implementando el inclinómetro en Arduino

Primero vamos a cablear nuestro acelerómetro de la siguiente manera:
  • X -> Analog0
  • Y -> Analog1
  • Z -> Analog2
  • SL -> Pin 3.3V del acelerómetro
  • 0G -> Desconectado
  • 5V -> 5V Arduino
  • 3.3V -> AREF Arduino
  • GS -> GND
  • ST -> GND
El circuito montado queda más o menos como el siguiente diagrama:
   

Nota importante: Este circuito es capaz de funcionar con 5V, sin embargo internamente funciona con 3.3V lo cual utilizaremos como referencia para el convertidor Análogo->Digital. Esto nos permitirá tener mayor precisión a la hora de leer los cambios de voltaje en las salidas análogas del acelerómetro.

Lo primero por hacer es configurar nuestro Arduino, vamos a incluir la biblioteca matemática para poder utilizar las funciones trigonométricas:

#include <math.h>

void setup() {
  analogReference(EXTERNAL);
  Serial.begin(9600);
}

int xVal = 0;
int yVal = 0;
int zVal = 0;

double angleYZ = 0;
double angleXZ = 0;

Definimos algunas variables para almacenar los valores de los componentes de las fuerzas en cada uno de los ejes del acelerómetro. Por último un par de variables de tipo flotante doble van a almacenar el ángulo en el que se encuentra el acelerómetro.

Vamos a utilizar el puerto serial para escribir los valores, esto lo necesitaremos luego al generar un pequeño programa en Python para visualizar gráficamente las inclinaciones.

Indicamos a nuestro Arduino que vamos a utilizar los 3.3V del acelerómetro como nuestro voltaje de referencia para la conversión Análoga->Digital.

Luego, en el loop principal tenemos que leer los valores análogos y hacer un pequeño ajuste utilizando la función map(). El problema es que el convertidor análogo-digital tiene un rango que va desde 0 a 1023, si utilizáramos la función arco tangente utilizando estos valores nos mentiría ya que los componentes de la fuerza solo tendrían valores positivos y por lo tanto valores erróneos.

Para corregir este problema utilizamos la función "map()" que nos permitirá que nuestro rango varíe entre valores positivos y negativos como se muestra en la siguiente tabla:

Fuerza GVoltaje salidaArduino analogRead()Luego de map()
1.5~3.3V~1023~500
............
0~1.6V~511~0
............
1.5~0V~0~-500

¿Por qué elegir entre -500 y 500?  Realmente elegí este rango para mantener más o menos igual el "rango" original. Si el convertidor A->D tiene 1024 pasos y quiero mantener +/- la misma precisión lo ideal sería tener un nuevo rango que tuviera 1024 pasos, el rango ideal entonces hubiera sido desde -511 hasta 511, pero como quería números fáciles de recordar decidí elegir desde -500 hasta 500.

Una vez dicho esto el código queda de la siguiente manera:
void loop() {
  
  xVal = analogRead(0);    
  yVal = analogRead(1);    
  zVal = analogRead(2);   
 
  xVal = map(xVal, 0, 1023, -500, 500); 
  yVal = map(yVal, 0, 1023, -500, 500); 
  zVal = map(zVal, 0, 1023, -500, 500); 
  
  angleYZ = atan((double)yVal / (double)zVal);
  angleYZ = angleYZ*(57.2958);
  
  angleXZ = atan((double)xVal / (double)zVal);
  angleXZ = angleXZ*(57.2958);
  
  Serial.write("yz:");
  Serial.print(angleYZ);
  Serial.write("\n");
  
  Serial.write("xz:");
  Serial.print(angleXZ);
  Serial.write("\n");
  
  delay(100);  
}

Como pueden ver en el código lo primero que hacemos es leer los valores que se encuentran en las entradas análogas, luego utilizamos la función map para generar un rango más adecuado para los valores de entrada y por último aplicamos las sencillas funciones trigonométricas para calcular el ángulo correspondiente.

Una vez calculados los ángulos simplemente los imprimimos en el puerto serial para poder leerlos desde la computadora.

Si revisan la salida del puerto serial notarán que los valores varian "bastante", esto sucede ya que siempre hay algo de "ruido" o interferencias en la señal y es completamente normal. Para recomendaciones sobre como reducir el ruido les recomiendo leer la hoja técnica que se referencia al final de esta entrada.

Hasta este punto simplemente compilamos nuestro programa y lo guardamos en nuestro Arduino. Pueden descargar el código desde la siguiente dirección:


Visualizando las inclinaciones con Python


Por último vamos a crear una pequeña interfaz para visualizar las inclinaciones. Para ello vamos a utilizar varias librerías y algo del código que ya habíamos utilizado antes para el seguro programable con Arduino para la lectura de datos desde el puerto serial.

Para la interfaz gráfica vamos a utilizar la biblioteca pygame, esta es una sencilla biblioteca que permite dibujar gráficos en pantalla. Está originalmente diseñada para crear juegos sencillos y provee de muchas funciones muy fáciles de utilizar.

Inicializando pygame


Lo primero que haremos es cargar todas las bibliotecas necesarias y e inicializar la biblioteca:

import pygame, sys, math, serial, threading
from pygame.locals import *

# Inicializando PyGame
pygame.init()
SCR_WIDTH = 640
SCR_HEIGHT = 480
COLOR1 = (255, 255, 255)
COLOR2 = (0, 0, 0)
COLOR3 = (255, 0, 0)
DISPLAYSURF = pygame.display.set_mode((SCR_WIDTH,SCR_HEIGHT))
DISPLAYSURF.fill(COLOR2)
pygame.display.set_caption('Inclinometro Digital!')

En este punto definimos algunas variables globales como el ancho y alto de la pantalla y algunos colores sencillos. La variable DISPLAYSURF es la que vamos a utilizar para dibujar, pueden ver como la primera orden es fill (rellenar) el fondo de negro.

Monitoreando el puerto serial


Para la lectura de datos del puerto serial vamos a utilizar un par de funciones "ayudante" y el proceso de lectura se correrá en un hilo de ejecución separado, así tendremos el hilo principal encargado de dibujar la interfaz del usuario y un hilo secundario encargado de leer los datos del puerto serie y actualizar las variables que guardan el ángulo del inclinómetro.
ser = serial.Serial(
  '/dev/ttyUSB0',
  baudrate=9600,
  interCharTimeout=None
)
t = threading.Thread(target=receiving,args=(ser,)).start()

Nota: En mi computadora el Arduino se detecto en el puerto /dev/ttyUSB0, recuerden cambiar al puerto correspondiente antes de correr el script.

La función receiving se encarga de leer la última línea disponible en el puerto serial, esta función la hemos copiado de nuestro seguro programable en Arduino:
def receiving(ser):
  global last_received
  buffer = ''
 
  while True:
    buffer += ser.read(ser.inWaiting())
    if '\n' in buffer:
      lines = buffer.split('\n')
      last_received = lines[-2]
      buffer = lines[-1]
      store_angle(last_received.strip())

La función store_angle se encarga de leer la línea recibida del puerto serie y si corresponde al angulo entre yz o xz guardará el valor en la variable correspondiente:
angle_yz = 0.0
angle_xz = 0.0

last_received = ''

def store_angle(string):
  global angle_yz, angle_xz
  line = string.split(':')
  if len(line) > 1 :
    if line[0] == "yz":
      angle_yz = float(line[1])
    if line[0] == "xz":
      angle_xz = float(line[1])
    print string

Dibujando la interfaz gráfica

Las biblioteca pygame provee varias funciones de "dibujo" de primitivas, esto es que nos permite dibujar figuras básicas como líneas, polígonos y círculos dentro de una superficie.

La lógica de las interfaces gráficas en pygame es muy simple: Revisamos si hay alguna acción del usuario que tengamos que manejar y luego dibujamos en pantalla, repetimos esto de manera indefinida.

while True: # Loop principal
  for event in pygame.event.get():
    if event.type == QUIT:
      ser.close()
      pygame.quit()
      sys.exit()

  # Limpiar pantalla
  DISPLAYSURF.fill(COLOR2)

  draw_box(SCR_WIDTH/3,SCR_HEIGHT/2,200,200)
  draw_angle(angle_yz,SCR_WIDTH/3,SCR_HEIGHT/2,100)

  draw_box(2*(SCR_WIDTH/3),SCR_HEIGHT/2,200,200)
  draw_angle(angle_xz,2*(SCR_WIDTH/3),SCR_HEIGHT/2,100)

  pygame.display.update()

El código anterior realiza algunas funciones muy básicas. Primero revisa si hay un evento que nos obligue a "terminar" con la ejecución del programa. Si este es el caso se cierra la conexión al puerto serial, detenemos pygame y salimos del programa. 

Luego como se supone que el hilo de lectura del puerto serial ya está obteniendo los valores de inclinación, simplemente llamamos a dos funciones ayudantes, la primera "draw_box" dibuja una caja de referencia con líneas indicando los ángulos.  Y la segunda "draw_angle" dibuja una línea que tiene la inclinación que especifiquemos.

Notarán que utilizo tamaños "relativos", esto es porque quiero hacer el programa fácilmente escalable y nada mejor que utilizar medidas relativas en vez de absolutas.

Función draw_box:
def draw_box(x_origin,y_origin,width,height):
  # Caja
  pygame.draw.polygon(
      DISPLAYSURF,
      COLOR1,
        [
          (x_origin-(width/2), y_origin-(height/2)),
          (x_origin+(width/2), y_origin-(height/2)),
          (x_origin+(width/2), y_origin+(height/2)),
          (x_origin-(width/2), y_origin+(height/2)),
          (x_origin-(width/2), y_origin-(height/2))
        ],
      1
    )
  # Diagonal /
  pygame.draw.line(
      DISPLAYSURF,
      COLOR1,
      (x_origin-(width/2), y_origin-(height/2)),
      (x_origin+(width/2),y_origin+(height/2)),
      1
    )
  # Diagonal \
  pygame.draw.line(
      DISPLAYSURF,
      COLOR1,
      (x_origin+(width/2), y_origin-(height/2)),
      (x_origin-(width/2), y_origin+(height/2)),1
    )
  # Línea vertical |
  pygame.draw.line(
      DISPLAYSURF,
      COLOR1,
      (x_origin, y_origin-(height/2)),
      (x_origin, y_origin+(height/2)),
      1
    )
  # Línea horizontal --
  pygame.draw.line(
      DISPLAYSURF,
      COLOR1,
      (x_origin-(height/2),y_origin),
      (x_origin+(height/2),y_origin),
      1
    )

Función draw_angle:
def draw_angle(angle,x_origin,y_origin,lenght):
  x_len = lenght*math.cos(angle*0.01745)
  y_len = lenght*math.sin(angle*0.01745)
  pygame.draw.line(
      DISPLAYSURF,
      COLOR3, 
      (x_origin-x_len,y_origin-y_len), 
      (x_origin+x_len,y_origin+y_len),
      3
      )

Notarán que en la última función utilizo las funciones trigonométricas nuevamente para obtener las coordenadas correspondientes.

Al final, si todo ha salido bien al ejecutar el script tendrán una pantalla como la siguiente:


Pueden descargar el código de la aplicación de la siguiente dirección:

Concluyendo

Los acelerómetros son dispositivos que se encuentran en muchísimos dispositivos modernos. Desde celulares, laptops, tablets, aunque parescan "complicados" de utilizar abren grandes posibilidades de nuevas formas de interacción con los usuarios.

No habiendo más que decir por ahora... ¡Hasta la Próxima!

Referencias


17 comentarios:

Juanfer R. dijo...

Ola podria facilitarnos el montaje
gracias

Mario Gómez dijo...

¡Hola Juanfer R!

Ya subí el diagrama de referencia.

¡Saludos!
Mario.

Felipe Andres Farias Urzua dijo...

Muchas gracias mario por compartir tus conocimientos de manera tan sencilla
me parecio una muy buena explicacion.


solo una acotación en tu diagrama(2) de fuerzas ocupas los ejes X-Z y luego en lo que queda de desarrollo ocupas Y-Z

Mario Gómez dijo...

Gracias por el comentario Felipe, ya he corregido el texto en la entrada. Cambiaré el segundo diagrama cuando llegue a casa para que sea consistente con el primero.

¡Saludos!
Mario.

Oscar Gerardo Barajas González dijo...

xq multipliaste por 57.29??

Mario Gómez dijo...

¡Hola Oscar!

Lo que sucede es que todos los resultados son en radianes, entonces multiplico por 57.2958 para convertir en grados decimales.

¡Saludos!
Mario

Gefry Andres Castro Jimenez dijo...
Este comentario ha sido eliminado por el autor.
Gefry Andres Castro Jimenez dijo...

Amigo un pregunta, que softwate utilizas para programar python, la verdad no se mucho de eso.. y ps me gustaria que funcionara con pygame...

victor colorado dijo...

victor colorado
que software ocupaste del python porque no me deja abrirlo
gracias

victor colorado dijo...

quisiera mas información del simulador ya que en la en la escuela me pidieron un simulador igual

Carlos Vallejo dijo...

Como podría observar las gráficas de inclinación en un display grafico sin necesidad de la pc????

Federico dijo...

Hola que tal?, me tira error el python que no le encuentro problema. el error en concreto es:
angle_yz = float(line[1])
ValueError: invalid literal for float(): 7.31 xz

Que compilador usaste?

Misael Alarcon dijo...
Este comentario ha sido eliminado por el autor.
Misael Alarcon dijo...

Hola Mario. Gran post, solo tengo una pregunta ¿cómo puedo saber el puerto USB donde se detecta mi Arduino? Por el compilador de arduino sé que es COM3 pero ¿cuál es la ruta? Gracias.

lordjayc dijo...

Hola Maria, muchas gracias, entendi mucho sobre la programacion.
PD: Tienes algun post sobre como programar este acelerometro con un servomotor?
Te agradezco de nuevo, saludos

Guillote dijo...

Consulta si tiene sensibilidad de 800mv/g en 1.5 G no seria 2,85V la tensión aproximadamente? Muy bueno el post.

Carolina Herrero dijo...

Hola, la conexión tiene que ser así?esque he visto otros ejemplos de uso de este sensor que solo trabajan con x,y,z,5V y GND.

Gracias de antemano.