Autor Tema: Visualización de datos en tiempo real.  (Leído 2844 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Visualización de datos en tiempo real.
« en: 16/May/2015, 16:36:56 pm »
>> Vídeo de demostración en el post siguiente <<

He seguido experimentando con el ADC de 22 bits que me ha regalado Carlos, y he creado un capturador de datos vía python que es bastante interesante. Antes que nada, voy a explicar el montaje.

El hilo donde se explica el circuito está en esta respuesta a otro hilo.

La idea fundamental es conectar un ADC MCP3553, de 22 bits de resolución, a una referencia de voltaje de precisión ADR421, y enviar los datos de medida a través de Arduino a un programa que los represente gráficamente en tiempo real.

El arduino recoge las lecturas de 50 en 50, en forma de grupos de 3 bytes, y las envía en formato binario a través del puerto serie a la velocidad maxima, para evitar retrasos e interrupciones. Cada envío tiene una suma de comprobación de 16 bits, y un timestamp dado por la función millis() del arduino, por si se quieren ordenar los datos con mayor precision (por ejemplo, para hacer un análisis espectral.)

Una vez que los datos llegan al ordenador, se verifica la suma de comprobación, se convierten los datos a doble precisión, se calcula su media, su varianza, y un intervalo de confianza del 95% para la media. Opcionalmente, se representan en tiempo real a través de matplotlib.

En el post siguiente pondré un enlace a youtube con el aparato en funcionamiento.

Veamos primero el código fuente en Arduino: el micro recibe los datos del conversor a través de SPI, los almacena en un buffer, y finalmente los envía por Puerto serie en paquetes. El código es muy sencillo:

Código: (Arduino) [Seleccionar]
#include <SPI.h>
/* D13 -> SCK
 * D12 -> MISO
 * D11 -> MOSI (UNUSED)
 * D9  -> SS
 */
#define SCKPIN 13
#define MISOPIN 12
#define MOSIPIN 11
#define SSPIN 9

#define BUFFSIZE 50                 // Don't exceed 84. 'i' in loop is char.
#define BUFFLIM (BUFFSIZE * 3)      // Buffer's first checksum byte.
#define BAUDRATE 115200             // Serial baud rate.
#define SPIRATE 100000              // SPI baud rate.

SPISettings settingsA(SPIRATE, MSBFIRST, SPI_MODE3);

unsigned char buff[3 * BUFFSIZE + 2];
union {
  unsigned long time;
  unsigned char bytes[4];
  } timestamp;

void setup() {
  pinMode (SSPIN, OUTPUT);
  SPI.begin();
  Serial.begin(BAUDRATE);
}


void loop() {
  long ret;
  double vout;
  unsigned char flow = 0, fhigh = 0;  // Checksum bytes.
  unsigned char i = 0, tmp;
 
  while (1) {
    SPI.beginTransaction(settingsA);
    digitalWrite (SSPIN, LOW);
    while (digitalRead(MISOPIN) != LOW); // Wait for ADC ready.
   
    // Read data, compute checksum:
    tmp = SPI.transfer(0);
    flow  += tmp;           // Fletcher's checksum, modulo 256.
    fhigh += flow;
    buff[i++] = tmp;
    tmp = SPI.transfer(0);
    flow  += tmp;
    fhigh += flow;
    buff[i++] = tmp;
    tmp = SPI.transfer(0);
    flow  += tmp;
    fhigh += flow;
    buff[i++] = tmp;

    if (i == BUFFLIM) {  // Send burst of data.
      buff[i++] = flow;
      buff[i]   = fhigh;
      timestamp.time = millis();
      Serial.write(timestamp.bytes, 4);       // Timestamp.
      Serial.write(buff, 3 * BUFFSIZE + 2); // Dump data.
      i = 0;
      flow = 0;
      fhigh = 0;
    }

    SPI.endTransaction();
    digitalWrite (SSPIN, HIGH);
 
    delay(20);
 }
}

El código fuente en python del receptor es algo más complejo, aunque tampoco nada del otro mundo. A través de pySerial, el programa se sincroniza con los paquetes que envía Arduino, y luego va recibiendo y procesando los datos.

Código: (Python) [Seleccionar]
import serial
import struct
from scipy import stats
from matplotlib import pyplot
from matplotlib import pylab


class Connection(object):
 
  # Serial data.
  serialPort    = 'COM3'   # Serial port to use.
  baudRate      = 115200   # Serial port's baud rate.
  serialTimeout = 12.6     # Serial port tiemout in seconds.

  # Packet data.
  samplesPerPacket = 50
  packetSize       = 4 + samplesPerPacket * 3 + 2

  totalData = list()
  data = list()

  # pylab niceties:
 

  def __init__(self):
    self.lastPacket = None
    # Chi-square 95% confidence bound:
    self.chi2ppf = stats.chi2(df = self.samplesPerPacket - 1).ppf(0.05)
    # Standard normal distribution 95% bound:
    self.normppf = stats.norm().ppf(0.975)   
    self.connect()
   
  def listen(self, packets = -1):
    self.sync()
    while (packets != 0):
      packets = packets - 1
      self.read()
      self.packetDisplay()
   
  def connect(self):
    try:
      self.port = serial.Serial(self.serialPort,
                                self.baudRate,
                                timeout = self.serialTimeout)
    except:
      print "Couldn't open port: ", self.serialPort     

  def sync(self):
    self.port.flushInput()
    while (True):
      if (self.hard_read()): break
      print "sync: retry. (read %d of %d bytes.)" % ( len(self.lastPacket),
                                                      self.packetSize)
      self.port.flushInput()

  def hard_read(self):
    self.lastPacket = self.port.read(self.packetSize)
    if (self.packetSize == len(self.lastPacket)): return True
    return False

  def read(self):
    if (not self.hard_read()):
        print "read - resync."
        self.sync()

  def packetDisplay(self):
    if (self.lastPacket == None):
      return None
    print "\nTimestamp: %d" % struct.unpack("<I", self.lastPacket[0:4])[0]
   
    if (self.cksum()): print "Checksum ok."
    else: print "Checksum failed!"


    for x in self.data:
      self.totalData.append(x)
       
    self.data = list()
    for i in range(4, 4 + 3 * self.samplesPerPacket, 3):
      self.data.append(self.unpackSample(self.lastPacket[i],
                                         self.lastPacket[i+1],
                                         self.lastPacket[i+2]))
    print self.data
    self.update_statistics()
    print "Average  = %8.8f" % self.average
    print "sigma = %8.8f" % (self.variance)**0.5
    print "95%% confidence bound = %8.8f" % self.bounds


  def unpackSample(self, x, y, z):
    val = (ord(x) << 16) + (ord(y) << 8) + ord(z)
    if (val & 0x200000): val = -(val & 0x1fffff)
    return 2.5 * val / 2097152.0

  def cksum(self):
    flow  = 0
    fhigh = 0
    for i in range(4, 4 + 3 * self.samplesPerPacket):
      flow += ord(self.lastPacket[i])
      fhigh += flow
      flow  = flow  & 0xff
      fhigh = fhigh & 0xff
    if ((flow  == ord(self.lastPacket[-2])) and
        (fhigh == ord(self.lastPacket[-1]))):
      return True
    return False

  def update_statistics(self):
    self.update_average()
    self.update_variance()
    self.update_bounds()

  def update_average(self):
    self.average = 0.0
    for x in self.data:
      self.average += x
    self.average = self.average / self.samplesPerPacket

  def update_variance(self):
    self.variance = 0.0
    for x in self.data:
      self.variance += (x - self.average)**2
    self.variance = self.variance / self.samplesPerPacket

  def update_bounds(self):  # 95% bonunds to samples.
    self.bounds =  self.normppf * (self.variance / self.chi2ppf)**0.5

  def plot(self):
    pylab.plot(range(0,len(self.totalData)), self.totalData)
    pylab.show()

  def capture(self, packets):
    pyplot.ion()
    fig = pyplot.figure()
    sp = fig.add_subplot(111)
    li, = sp.plot(range(0,len(self.data)), self.data)
    self.sync()
    fig.canvas.draw()
    pyplot.show(block=False)
    while (packets != 0):
      packets = packets - 1
      self.read()
      self.packetDisplay()
      try:
        li.set_xdata(range(0, len(self.totalData)))
        li.set_ydata(self.totalData)
        sp.relim()
        sp.autoscale_view(True, True, True)
        fig.canvas.draw()
        pyplot.pause(0.1)  # Evita bloqueo de ventana.
      except KeyboardInterrupt:
        break

  def clear(self):
    self.totalData = []

Para usar el código, sugiero descargarse una version de Portable Python (preferentemente la 2.7.6), que ya tiene incluidos el pySerial, sciPy y matplotlib. Se importa nuestro código fuente, y luego, para conectar, simplemente se instancia la clase de conexión:

>>> conn = Connection()

A continuación, se pueden capturar los datos gráficamente con:

>>> conn.capture(100)

donde 100 es el número de paquetes a procesar. Los datos capturados quedan en el objeto de conexión, en la lista conn.totalData.

El programa admite muchas mejoras, pero demuestra bastante bien el concepto. Un ejemplo de cómo funciona en el próximo post.
« Última modificación: 16/May/2015, 17:30:43 pm por Manuel »

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Re:Visualización de datos en tiempo real.
« Respuesta #1 en: 16/May/2015, 17:29:46 pm »
Aquí está el video de funcionamiento, un poco en plan selfie pero funciona:

[youtube]-IVfldxfaAk[/youtube]

Y aquí una salida de una típica captura de datos:

Código: [Seleccionar]
Timestamp: 61306
Checksum ok.
[0.5655205249786377, 0.5655336380004883, 0.5655169486999512,
0.5655169486999512, 0.5655264854431152, 0.5655372142791748,
0.5655241012573242, 0.5655169486999512, 0.5655157566070557,
0.5655276775360107, 0.5655157566070557, 0.5655133724212646,
0.5655252933502197, 0.5655109882354736, 0.5655157566070557,
0.5655336380004883, 0.5655062198638916, 0.5655074119567871,
0.5655097961425781, 0.5655002593994141, 0.5655050277709961,
0.5655133724212646, 0.5655348300933838, 0.5655157566070557,
0.5655097961425781, 0.5655288696289062, 0.5655014514923096,
0.5655086040496826, 0.5655109882354736, 0.5655229091644287,
0.5655014514923096, 0.5655121803283691, 0.5655300617218018,
0.5655002593994141, 0.5655431747436523, 0.5655241012573242,
0.5655062198638916, 0.5655181407928467, 0.5655241012573242,
0.5655407905578613, 0.5655217170715332, 0.5655050277709961,
0.5655097961425781, 0.5655097961425781, 0.5655252933502197,
0.5655050277709961, 0.565495491027832, 0.5655324459075928,
0.5655121803283691, 0.5655074119567871]
Average  = 0.56551702
sigma = 0.00001134
95% confidence bound = 0.00000381

Una vez que se toma la media de 50 observaciones, la precision es realmente buena.

Carlos

  • Moderador Global
  • ****
  • Mensajes: 294
Re:Visualización de datos en tiempo real.
« Respuesta #2 en: 16/May/2015, 18:41:12 pm »
Ha quedado estupendo  ;)
Enhorabuena por el proyecto.

Veo que has reducido bastante el ruido.
Si lo quieres reducir más por software ¿Has probado técnicas de filter decimation?

Un saludo.

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Re:Visualización de datos en tiempo real.
« Respuesta #3 en: 16/May/2015, 23:28:14 pm »
Muchas gracias  :D. Al tomar la media de 50 observaciones, se está haciendo un diezmado que aporta una precisión adicional de log(50)/log(4) = 2.82 bits, con la frecuencia limitada a unos 0.25 hertzios. El ADC está haciendo unas 25 muestras por segundo, cuando el límite está en 50. Creo que voy a jugar con el código fuente en C, en la parte del delay final, para acercarme al límite de muestras por segundo, a ver cuánto se puede sacar. De todos modos, como me has dicho, el ruido de mi circuito está muy por encima de lo necesario, así que seguro que se sacará mucha más precisión con los condensadores de desacoplamiento que me has recomendado. A ver cuándo saco un rato para soldarlos.

¡Muchas gracias y un saludo!

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Re:Visualización de datos en tiempo real.
« Respuesta #4 en: 18/May/2015, 18:41:43 pm »
Hola. Para estabilizar el circuito, he añandido un venerable regulador uA723 (con ruido por debajo de los 3uV) alimentada por pilas, he separado el suelo analógico del digital, incluyendo un par de ferritas, y he practicado algunos desacoplamientos en plan salvaje.

El resultado es que ahora el ruido RMS está en, y muchas veces por debajo, de los 10uV  ;D

Pero en la última hora que he estado experientando, la medida de voltaje tiene un claro drift negative de 1uV/s  :P

Para evitar las fluctuaciones salvajes del diodo, he medido una resistencia estándar del chino durante más de seis minutos, y ésto es lo que he obtenido (he mejorado la toma de datos hasta casi 50 muestras por segundo):

IMAGEN DEL DRIFT EN  6.56 MINUTOS.

El circuito, en su forma actual, es:

EL CIRCUITO CON uA723 Y DESACOPLAMIENTO.

¿Se te ocurre algún motive del drift? No sé si es la variación de temperature en mi casa (me está dando el solazo en la persiana de mi laboratorio), algún condensador electrolítico que no se acaba de cargar (el uA723 está limitado a unos 14 mA de corriente, y esto lleva más de cuarenta minutos cayendo, así que no creo que sea el motivo), que no he tenido espacio para añadir un condensador electrolítico en la entrada de la batería, la estabilización térmica del uA723, el demonio de Maxwell jugando con mis electrones, o qué?  :o

En todo caso, creo que me estoy empezando a acercar a la auténtica resolución del ADC, porque las salidas de datos son del tipo:

Código: [Seleccionar]
Timestamp: 400218
Checksum ok.
[0.3715860843658447, 0.3715789318084717, 0.3715991973876953,
0.3715968132019043, 0.37160754203796387, 0.3715837001800537,
0.37159204483032227, 0.37158966064453125, 0.37160277366638184,
0.3715956211090088, 0.37160158157348633, 0.3715837001800537,
0.37158846855163574, 0.37158846855163574, 0.3716003894805908,
0.3716099262237549, 0.37160158157348633, 0.3715837001800537,
0.37160277366638184, 0.37158727645874023, 0.37161827087402344,
0.3715956211090088, 0.37158727645874023, 0.3715860843658447,
0.3715991973876953, 0.3715932369232178, 0.37160158157348633,
0.37159204483032227, 0.3715944290161133, 0.3715956211090088,
0.3715944290161133, 0.37160277366638184, 0.3716158866882324,
0.3715980052947998, 0.37160634994506836, 0.3715980052947998,
0.3715956211090088, 0.3715932369232178, 0.37157654762268066,
0.3715813159942627, 0.3716003894805908, 0.3715860843658447,
0.3715968132019043, 0.3715932369232178, 0.3715789318084717,
0.3715825080871582, 0.37160634994506836, 0.37157773971557617,
0.37159085273742676, 0.37160634994506836]
Average  = 0.37159450
sigma = 0.00000955
95% confidence bound = 0.00000321
La sigma da el error RMS, que está justo por debajo de 10uV, con drift y todo.

Resuelto: (?) Vaya, había olvidado que el uA723 necesita como mínimo 9.5 voltios de alimentación. Le he puesto dos pilas de 9 voltios en serie, y ahora está funcionando de maravilla. Pero tendré que ensayar más.


« Última modificación: 18/May/2015, 19:56:09 pm por Manuel »

Carlos

  • Moderador Global
  • ****
  • Mensajes: 294
Re:Visualización de datos en tiempo real.
« Respuesta #5 en: 18/May/2015, 19:57:24 pm »
En las pruebas que he hecho, he alimentado el regulador de voltaje de precisión directamente de la batería, la misma que alimenta al Arduino, y no me ha dado problemas.

En todo caso el ruido de ese ADC debe estar en 8uVrms, creo que se parece bastante a lo que te sale:

Código: [Seleccionar]
0.371 586
0.371 578
0.371 599
0.371 596
0.371 607
0.371 583
0.371 592
0.371 589
0.371 602
0.371 595
0.371 601
0.371 583
0.371 588
0.371 588
0.371 600
0.371 609
0.371 601
0.371 583
0.371 602
0.371 587
0.371 618
0.371 595
0.371 587
0.371 586
0.371 599
0.371 593
0.371 601
0.371 592
0.371 594
0.371 595
0.371 594
0.371 602
0.371 615
0.371 598
0.371 606
0.371 598
0.371 595
0.371 593
0.371 576
0.371 581
0.371 600
0.371 586
0.371 596
0.371 593
0.371 578
0.371 582
0.371 606
0.371 577
0.371 590
0.371 606


¿Dónde has puesto los condensadores en el ADC? hay que poner 3

No es necesario separar parte analógica y digital porque cada una actúa en diferentes momentos con el "single conversion mode". Primero convierte y luego envía el dato antes de volver a convertir.

Puede que te venga bien sincronizar el ADC con el paso por cero de la frecuencia de red. Lo malo de ese ADC tan rápido es que no elimina bien los ruidos de 50 Hz.
« Última modificación: 18/May/2015, 20:11:55 pm por Carlos »

Carlos

  • Moderador Global
  • ****
  • Mensajes: 294
Re:Visualización de datos en tiempo real.
« Respuesta #6 en: 18/May/2015, 20:09:36 pm »
No entiendo bien la imagen.
¿Es la tensión del diodo con el tiempo?
Probablemente estás midiendo temperatura. A 2mV/ºC, ese drift es igual a 0.2ºC.

Un saludo.

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Re:Visualización de datos en tiempo real.
« Respuesta #7 en: 18/May/2015, 20:42:19 pm »
[Respuesta al hilo anterior: no, es la medida de un par de resistencias, 10K y 820R, que hacen una division de voltaje. Su sensibilidad a la temperatura es muy baja.]

Hola. No le he soldado los condensadores, porque quería esperar a tener el mínimo de ruido por otros caminos. Al final parece que el drift se debía a una tensión insuficiente en en uA723. Tras corregir el problema, he logrado varios recorridos con un drift despreciable. Por ejemplo:

15000 samples sin drift aparente.

En este recorrido, el ruido total de pico a pico es menor que 90 microvoltios, lo que comparado con los 500uV de pico a pico que tenía antes de todo el montaje no está nada mal.

Mi referencia de voltaje sólo tiene 2.5 voltios, mientras que el MCP3553 necesita como mínimo 2.7V. No puedo usarla para alimentar el ADC. Tengo un par de referencias de precisión de 5V, pero están en SOIC y no las tengo montadas. Además, creo que son menos precisas que mi ADR421.

A la velocidad a la que estoy convirtiendo, no estoy seguro de que el ringing de la comunicación por SPI no meta ruido en el conversor. Creo que he mejorado la estabilidad en casi un factor de 10 al hacer toda la separación. Y he tenido alguna sorpresa desagradable al dejar entrar el ruido de Arduino en la conversión.

En la última pasada, el resultado final ha sido:

Código: [Seleccionar]
Timestamp: 398902
Checksum ok.
[0.3854405879974365, 0.3854358196258545, 0.38544178009033203,
0.38544654846191406, 0.38543224334716797, 0.38543701171875,
0.38544416427612305, 0.3854525089263916, 0.3854238986968994,
0.385434627532959, 0.3854513168334961, 0.38544654846191406,
0.38544297218322754, 0.3854358196258545, 0.385439395904541,
0.38544654846191406, 0.38544297218322754, 0.3854513168334961,
0.38544654846191406, 0.3854501247406006, 0.385439395904541,
0.3854489326477051, 0.38544654846191406, 0.3854489326477051,
0.38543105125427246, 0.38544774055480957, 0.38543701171875,
0.3854334354400635, 0.38544178009033203, 0.38544654846191406,
0.3854560852050781, 0.3854548931121826, 0.3854548931121826,
0.38544178009033203, 0.3854405879974365, 0.38544297218322754,
0.38544535636901855, 0.38544774055480957, 0.385434627532959,
0.38544416427612305, 0.38543701171875, 0.3854513168334961,
0.38544297218322754, 0.38544297218322754, 0.38544535636901855,
0.3854501247406006, 0.38544535636901855, 0.38544297218322754,
0.3854525089263916, 0.3854548931121826]
Average  = 0.38544385
sigma = 0.00000688
95% confidence bound = 0.00000232

Final mean     : 0.385445586
Final sigma    : 0.000009645
Final 95% bound: 0.000000156

Los datos finales corresponden a las 15000 muestras, e indican un ruido RMS de unos 9.6uV que, teniendo en cuenta las fluctuaciones térmicas durante el recorrido, tampoco están tan mal. El siguiente paso es el de soldar los condensadores: ahora mismo sólo tengo una remesa que compré en ebay, y que me han traído una enorme mala suerte. Los últimos cinco proyectos que hice con ella, tres osciladores de RF y dos microcontroladores, fallaron miserablemente y no tiraron hasta que no usé componentes radiales. Tengo encargada una colección de condensadores SMD de buena calidad en Mouser, y prefiero esperar a que me lleguen, antes que soldar esta caca de componentes.

Muchas gracias de nuevo. Realmente tengo que poner una buena referencia en la entrada del ADC, porque actualmente sólo vale para medidas diferenciales respect a la fuente de alimentación.


« Última modificación: 18/May/2015, 20:44:20 pm por Manuel »

Carlos

  • Moderador Global
  • ****
  • Mensajes: 294
Re:Visualización de datos en tiempo real.
« Respuesta #8 en: 19/May/2015, 15:31:30 pm »
Parece que has conseguido llegar a la precisión máxima del integrado sin utilizar condensadores.
 ((:-))

Un saludo.
« Última modificación: 19/May/2015, 15:50:59 pm por Carlos »

Manuel

  • Colaborador
  • *
  • Mensajes: 23
Re:Visualización de datos en tiempo real.
« Respuesta #9 en: 23/May/2015, 15:22:36 pm »
Hola, Carlos. Perdona que tardara tanto en responder: esta semana he estado cerrando el proyecto de Arduino en mi centro, y es un montón de papeleo.

He probado el magnífico chip MCP3550-50 que me has mandado (¡gracias!). Es en efecto mucho mejor que el 3553. No me ha costado nada adaptar todo el tinglado para este aparato, y el resultado es:

El circuito con el 3550-50, que he soldado punto a punto: prefiero el método dead-bug a usar pcbs.

Los resultados son brutales: he medido exactamente las mismas resistencias, y en el gráfico se puede apreciar perfectamente la cuantificación de las medidas. Hay que tener en cuenta que python usa doble precisión real, así que la cuantificación no es un artefacto de la precisión en coma flotante.

5000 samples de 3550-50 sobre una division de voltaje.

El ruido RMS típico por cada Segundo es del orden de 3 a 5 uV RMS, y como puede verse en la gráfica, el ruido pico a pico es de 40uV en 7 minutos, y eso con las fluctuaciones añadidas.

Un magnífico aparato.