Tecnología > Electricidad y Electrónica

Visualización de datos en tiempo real.

(1/2) > >>

Manuel:
>> 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) ---#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);
 }
}

--- Fin del código ---

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) ---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 = []

--- Fin del código ---

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.

Manuel:
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: ---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

--- Fin del código ---

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

Carlos:
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:
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:
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: ---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

--- Fin del código ---
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.


Navegación

[0] Índice de Mensajes

[#] Página Siguiente

Ir a la versión completa