Prueba del acelerómetro MMA7260 con PIC y PyQwt

En la entrada anterior  se mostraron las características y el funcionamiento de este sensor. Ahora utilizaré la librería gráfica PyQwt para mostrar las mediciones que serán convertidas de analógico a digital mediante el PIC12F510 y serán transmitidas a una PC a través del estándar RS-232.

PIC12F510

El PIC12F510 es un microcontrolador producido por Microchip Technology. Este chip es de la familia Baseline y sus características mas importantes son:
    Encapsulado de 8 pines.
    Oscilador interno de precisión de 4/8 Mhz.
    3 canales ADC de 8 bits.
    Es un microcontrolador de 8 bits.
    Bajo consumo de corriente: 100 mA en modo Sleep.
    ICSP.
    El rango de voltaje es de 2.2V a  5.5V.
    Cuenta con un timer de 8 bits.

Este microcontrolador a pesar de ser de bajo costo, cumple con lo necesario para realizar este ejemplo con sus tres ADC de 8 bits.
La programación de este uControlador fue hecha en CCS y es muy sencilla, se puede dividir en tres pasos: Configuración del microcontrolador, Adquisición de datos y envío de datos a la PC. El programa completo se muestra a continuación:
mainp12510accel.h
  1. #include <12F510.h>
  2. #device adc=8
  3. #FUSES NOWDT                    //No Watch Dog Timer
  4. #FUSES INTRC                    //Internal RC Osc
  5. #FUSES NOPROTECT                //Code not protected from reading
  6. #FUSES MCLR                     //Master Clear pin enabled
  7. #FUSES IOSC8                    //INTOSC speed 8MHz
  8. #FUSES RESERVED                 //Used to set the reserved FUSE bits
  9. #use delay(clock=8000000)
  10. #use rs232(baud=9600,parity=N,xmit=PIN_B4,rcv=PIN_B5,bits=8)
mainp12510accel.c
  1. #include "mainp12510accel.h"
  2. void main()
  3. {
  4.    setup_adc_ports(AN0_AN1_AN2);
  5.    setup_adc(ADC_CLOCK_INTERNAL);
  6.    setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
  7.    setup_comparator(NC_NC);
  8.    int1 done;
  9.    int value;
  10.    delay_ms(1000);
  11.    while(1)
  12.    {
  13.       //EjeX
  14.       set_adc_channel(0);
  15.       delay_us(20);
  16.       value = read_adc();
  17.       done =adc_done();
  18.       while(!done)
  19.       {
  20.          done = adc_done();
  21.       }
  22.       putc(0x78); //letra X
  23.       putc(value);
  24.      
  25.       //EjeY   

PyQwt

Utilizaré PyQwt para mostrar gráficamente los datos obtenidos del sensor. Tomaré como base el ejemplo anterior donde se dibujó una onda senoidal, además, añadiré el modulo pyserial para la comunicación entra la PC y el microcontrolador y algunos qLcd para mostrar el valor numérico de la aceleración.
Comenzaré con el modulo pyserial. Su uso es muy sencillo, creo que con el ejemplo de su pagina web, en el punto pequeña introducción es suficiente:
Abrir el puerto nombrado a “19200,8,N,1s de tiempo de espera
  1. >>> ser = serial.Serial('/dev/ttyS1', 19200, timeout=1)
  2. >>> x = ser.read()          # leer un byte
  3. >>> s = ser.read(10)        # leer hasta 10 bytes (timeout)
  4. >>> line = ser.readline()   # leer un '\n' final de linea
  5. >>> ser.close()
Lo que hace este ejemplo es abrir el puerto serial, en este caso ttyS1(para Windows seria el COM1) a 19200 bps y con un tiempo de espera de 1 segundo. Con la instrucción read() leemos lo que haya en el buffer de recepción, con read(n), leemos un numero de bytes definido por n, con readline(), leemos una línea de texto (tiene que estar terminada por un caracter fin de línea \n), con close(), cerramos el puerto y lo dejamos libre. Para escribir algo en el puerto se usa write(“cadena”). Con esto se cubren los aspectos básicos de este modulo y es lo que utilizaremos normalmente.
Después dibujaré en QtDesigner la plantilla donde se incrustarán las gráficas, los qLcd’s, tres etiquetas para indicar los ejes XYZ y un menú con varias acciones (en Qt los menús se llaman actions). Imagen:
En la imagen anterior se aprecian tres submenús, Abrir puerto, Iniciar monitoreo y Salir. Ya que tenemos la interfaz gráfica definida sigo con el programa:
  1. import sys, osglob,  serial,  time
  2. from PyQt4 import QtCore, QtGui
  3. from Ui_testanalogui import Ui_MainWindow
  4. import PyQt4.Qwt5 as Qwt
  5. class testAnalogUI(QtGui.QMainWindow):
  6.     def __init__(self, parent=None):
  7.         QtGui.QWidget.__init__(self, parent)
  8.         self.ui = Ui_MainWindow()
  9.         self.ui.setupUi(self)
  10.        
  11.         #variables self
  12.         self.timer = QtCore.QTimer()
  13.         self.xdataXAxis  = [] #guarda los datos del eje x que seria el numero de muestra
  14.         self.ydataXAxis= [] #guarda los datos del eje y que seria los milivolts registrados
  15.         self.xdataYAxis  = [] #guarda los datos del eje x que seria el numero de muestra
  16.         self.ydataYAxis= [] #guarda los datos del eje y que seria los milivolts registrad
  17.         self.xdataZAxis  = [] #guarda los datos del eje x que seria el numero de muestra
  18.         self.ydataZAxis= [] #guarda los datos del eje y que seria los milivolts registrad
  19.        
  20.         #senales
  21.         self.connect(self.timer, QtCore.SIGNAL("timeout()"), self.timerEvent)
  22.         self.connect(self.ui.actionAbrir_puerto, QtCore.SIGNAL('triggered()'), self.manejoPuerto)
  23.         self.connect(self.ui.actionSalir, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
  24.         self.connect(self.ui.actionIniciar_monitoreo, QtCore.SIGNAL('triggered()'), self.toggleMonitoreo)
  25.     
  26.         #en esta variable se almacena el objeto plot devuelto por la funcion crearPlot( ) y el objeto curve
  27.         self.plotX, self.curveX = self.crearPlot('green')
El programa es muy sencillo de comprender, no tiene optimizaciones de código o funciones que puedan oscurecerlo. Aun así explicaré lo más relevante de cada una de las funciones empezando por:
def __init__(self, parent=None):
Aquí declaro cada una de las variables self  que seran accesibles en todo el programa. Por ejemplo las variables que almacenan los objetos QwtPlot puede ser modificadas en cualquier función, sin necesidad de pasarlas como argumentos o usar punteros. También creo el objeto QTimer, el cual genera la señal timeout() una vez que haya transcurrido una cantidad de tiempo definida por la instrucción start(tiempo). Usar un timer en lugar de crear un hilo, es más sencillo, pero es recomendable usar los hilos (threads) para aprovechar las ventajas del multiproceso. Un timer es semejante al objeto control timer de Visual Basic 6.0.
La conexión de las señales es una etapa importante en cualquier desarrollo con Qt, sin importar que lenguaje estemos utilizando. En este caso conectaremos las señales del QTimer y de los menús (actions) con las funciones que se ejecutarán al lanzarse la señal.
  1. self.connect(self.timer, QtCore.SIGNAL("timeout()"), self.timerEvent)
  2. self.connect(self.ui.actionAbrir_puerto, QtCore.SIGNAL('triggered()'), self.manejoPuerto)
  3. self.connect(self.ui.actionSalir, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
  4. self.connect(self.ui.actionIniciar_monitoreo, QtCore.SIGNAL('triggered()'), self.toggleMonitoreo)
En cada línea, una función es “conectada” a una señal emitida por un objeto. Por ejemplo, cada vez que se hace clic en el menú Abrir Puerto se ejecuta la función manejoPuerto.
def scanPorts(self):
Esta función es llamada por la func. manejoPuerto, lo que hace es buscar los puertos serie disponibles. El código esta basado en el ejemplo scan.py de pyserial.
  1. def scanPorts(self):
  2.         """scan for available ports. return a list of tuples (num, name)"""
  3.         #no va a numerar los puertos abiertos la otra clase de ejemplo si, pero utiliza cvtypes
  4.         available = []
  5.         if os.name == "nt":
  6.         
  7.             for i in range(256):
  8.                 try:
  9.                     s = serial.Serial(i)
  10.                     available.append( s.portstr)
  11.                     s.close()   # explicit close 'cause of delayed GC in java
  12.                 except serial.SerialException:
  13.                     pass
  14.         else:
  15.             available =  glob.glob('/dev/ttyS*') + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*')
  16.         return available
Como se aprecia en el comentario, este ejemplo no enumera los puertos abiertos, pero tampoco tengo que utilizar el modulo cvtypes. Lo modifiqué para que detectará bajo que sistema operativo se esta ejecutando, si es “nt” (windows) entonces los puertos apareceran como COM1, COM2, etc. La función glob del modulo glob, se utiliza para buscar dentro de un path (ruta), los archivos que coincidan con la cadena dada. También se pueden usar comodines como el “*”, que es el que utilice. Esto lo utilizo por que dependiendo de la distribución de Linux, es como se muestran los dispositivos seriales. Aunque en distribuciones de Linux basadas en Debian utilizan /dev/ttyS*, al utilizar el modulo FTDI (conversor serie usb), la ruta usada es /dev/ttyUSB*, por esta razón tambien se debe de incluir al enumerar los puertos.
def manejoPuerto(self):
En esta función es llamada por el menú Abrir Puerto. Si el puerto esta cerrado, lo configura y después lo abre, y si esta abierto, lo cierra.
  1.     def manejoPuerto(self):
  2.         if self.ui.actionAbrir_puerto.text()== 'Abrir puerto':
  3.             puertosDisponibles = self.scanPorts()
  4.             if len(puertosDisponibles) == 0:
  5.                 QtGui.QMessageBox.critical(None,
  6.                     self.trUtf8("Error"),
  7.                     self.trUtf8("""No existen puertos disponibles."""),
  8.                     QtGui.QMessageBox.StandardButtons(\
  9.                         QtGui.QMessageBox.Ok))
  10.                 return 0
  11.             item,  ok = QtGui.QInputDialog.getItem(self, self.trUtf8("Seleccionar puerto"),self.trUtf8("Seleccione el puerto:"),
  12.                 puertosDisponibles, 0, False)
  13.             if ok and not item.isEmpty():
  14.                 #Configuracion del puerto serie
  15.                 self.ptoSerial = serial.Serial(str(item), 9600, timeout = 1)
  16.                 self.ui.actionAbrir_puerto.setText("Cerrar puerto")
  17.                 self.ui.actionIniciar_monitoreo.setEnabled(True)
  18.         else:
  19.             self.timer.stop()
  20.             self.ui.actionAbrir_puerto.setText("Abrir puerto")
  21.             self.ui.actionIniciar_monitoreo.setEnabled(False)
  22.             self.ptoSerial.close()
Al hacer clic en el menú, aparece un QInputDialog donde se pueden seleccionar alguno de los puertos serie disponibles, estos se guardan en una lista y si esta lista esta vacía, aparecerá un QMessageBox que mostrará un mensaje de error. Una herramienta que utilizo frecuentemente, son los ayudantes que utiliza el IDE Eric4, donde se pueden crear, entre otros, los QMessageBox y los QInputBox. El puerto serie lo configuro a 9600 bps, y con un tiemout de 1 segundo. Los demás parámetros son por default: sin paridad, sin control por hardware, 8 bits.
def toggleMonitoreo(self):
El menú Iniciar monitoreo llama a esta función que inicia o para el timer, que esta programado a 10 ms.
  1.    def toggleMonitoreo(self):
  2.         if self.ui.actionIniciar_monitoreo.text() == 'Iniciar monitoreo':
  3.             #elimina el buffer del puerto para que no se grafiquen los datos almacenados porque aunque detenga el timer
  4.             #y no se siga graficando, el puerto sigue abierto y recibiendo datos en el buffer de entrada
  5.             self.ptoSerial.flushInput()
  6.             self.ui.actionIniciar_monitoreo.setText("Detener monitoreo")
  7.             self.timer.start(10)
  8.         else:
  9.             self.ui.actionIniciar_monitoreo.setText("Iniciar monitoreo")
  10.             self.timer.stop()
La instrucción flushInput() limpia el buffer de recepción, para que no se grafiquen datos leídos anteriormente.
def timerEvent(self):
Cada 10ms se ejecuta lo que este dentro de la función, en este caso, la función actualizarPlot().
  1. def timerEvent(self):
  2.         """timerEvent code"""
  3.         self.actualizarPlot()
def actualizarPlot(self):
La función actualizarPlot(), se encarga de leer los datos del puerto serie, decodificarlos, agregarlos a la grafica, a los lcd’s y redibujar la gráfica de tal forma que los datos se muestren en tiempo real.
  1. if (self.ptoSerial.inWaiting()):
  2.         #lee los datos del puerto serial correspondientes a los milivolts y los guarda en ydata
  3.         #lectura = ord(self.ptoSerial.read()) * 1.29411
  4.         lectura = self.ptoSerial.readline()
  5.         x, y, z = self.decodeData(lectura)
  6. . . . . . .
Si hay datos esperando en el buffer del puerto serie, entonces se leen los caracteres hasta encontrar un fin de línea “\n” y después se llama a la función que decodifica los datos leídos y los guarda en las variables x, y ,z.
  1. . . . . . . . .
  2. #eje y
  3.     self.ydataYAxis.append(y)
  4.     self.xdataYAxis.append(len(self.ydataYAxis))
  5.     self.curveY.setData(self.xdataYAxis, self.ydataYAxis)
  6. . . . . . . . .
Los datos de los ejes se anexan a la lista ydataYAxis, para el eje Y. Esto es el valor de la amplitud. Luego se cuentan los valores de la lista anterior, y se almacena en xdataYAxis. Esto es el valor del eje X. Estos dos listas corresponden a todos los valores que se tienen que graficar y se asignan a la grafica con la instrucción setData(x,y).
  1. . . . . . . . . . . .
  2. if (len(self.ydataXAxis)) > 250:
  3.     xax=len(self.ydataXAxis)-250
  4.     self.plotX.setAxisScale(Qwt.QwtPlot.xBottom,xax , len(self.ydataXAxis))
  5.     self.ydataXAxis.pop(0)
  6.     self.plotX.replot()
  7. . . . . . . . . . . . .
Para mostrar los datos en tiempo real se utilizan las líneas de código anteriores. Si el número de datos leídos del eje X ( Y, o Z) son mas de 250, se le resta 250, esto mantiene la línea al límite de la gráfica. Modificando este valor se puede mover la onda de la señal, por ejemplo, para que no sobrepase el final de la gráfica. Después se cambie la escala del eje X. Con la instrucción pop(), se elimina de la lista el último valor leído, de esta forma, la lista siempre tendrá un máximo de 250 elementos.
def decodeData(self, data):
Decodifica los datos leídos del puerto serie. Se esperan recibir 8 bytes ordenados de la siguiente manera: xnynxnCRCL. donde n representa el valor de la aceleración y la letra precedente, el eje leído. 1 byte es suficiente para leer cada eje, pues estoy usando un ADC de 8 bits. Los caracteres finales son el caracter retorno de línea y el final de línea.
  1. def decodeData(self, data):
  2.     if (len(data) <7):
  3.         print "Error"
  4.         return 0, 0, 0
  5.     x = ord(data[1]) * (330/255)
  6.     y = ord(data[3]) * (330/255)
  7.     z = ord(data[5]) * (330/255)
  8.     return x, y, z
Se esperan recibir mas de 7 bytes, si no se reciben, entonces se imprimirá en consola la palabra Error y se graficará con los datos 0, 0, 0. Esto esta hecho para evitar excepciones que interrumpan la ejecución del programa. El valor 330/255 * x equivale a una regla de tres donde se espera obtener un valor de 0 a 255 dependiendo de los milivolts detectados. En teoría el máximo valor de la aceleración corresponde a 255 y a 3.3 Volts. Pero en la practica esto no es así. No he medido el voltaje a la máxima aceleración posible.
def crearPlot(self, color):
Esta función es la misma que la de la entrada Plot con pyqt4 y qwt primera parte.
En la siguiente galería de imágenes, se muestra el comportamiento de las gráficas, dependiendo de la posición del sensor.

Descargar ejemplo

0 comentarios:

Publicar un comentario