Introducción
En este libro digital se explicará de manera detallada el uso de un osciloscopio armado a partir de una tarjeta ESP-32 y unos pocos resistores.
Hay que recordar que este osciloscopio está diseñado para ser de uso estudiantil y no para uso profesional por lo que se señalarán las limitantes del diseño así como las decisiones que se tomaron para llegar al diseño presentado.
Las únicas restricciones de diseño que se tuvieron al momento de comenzar el proyecto fueron: Presupuesto limitado (precio inferior a 300 mxn fechados 2021) Materiales accesibles o reutilizables para los estudiantes de la Facultad de Ingeniería Ningún herramental adicional (exceptuando una computadora con Linux, Mac o Windows) Posteriormente se agregó como meta el desarrollo de compatibilidad con Android en dispositivos móviles
El orden de este texto está optimizado como un manual se usuario por lo que quedará como:
-Marroja (AMR)
Manual de usuario
En el siguiente capítulo mostraremos paso a paso el uso y la preparación de los diferentes componentes del osciloscopio. Existen tres componentes principales:
- Armado físico (alambrado en la tarjeta proto-board)
- Programación de la tarjeta de desarrollo (por medio de Arduino)
- Programa graficador y generador de señales (ejecutables en una computadora personal)
Cómo armarlo
Para armar el osciloscopio necesitaremos:
- Una tarjeta ESP-32
- Un cable USB - Micro-USB (con conectividad de datos)
- Una tarjeta de modelado "proto-board" (puede ser de solo 30 filas)
- Cable (rojo y negro)
- 2 resistores de 3300 Ohmios
- 2 resistores de 220 Ohmios
- 2 resistores de 180 Ohmios
- 2 resistores de 51 Ohmios
Deberemos conectarlos de acuerdo al siguiente esquema:
Notas
Tenemos que tener cuidado con que todo esté correctamente conectado para asegurar el correcto funcionamiento del dispositivo. No dude en usar un multímetro para comprobar la conectividad entre los componentes de acuerdo al diagrama. También, considere que los resistores y el cable son los componentes más baratos de este dispositivo, merece la pena sacrificar esos componentes para un solo uso y asegurar que se coloquen de la mejor manera posible para garantizar la seguridad de la tarjeta ESP-32.
Cómo programarlo
Para programar la tarjeta ESP-32 necesitaremos una computadora personal (puede tener tanto Windows como OSX o Linux).
-
Tenemos que descargar e instalar el IDE de Arduino. Descarga
- Personalmente prefiero usar el IDE marcado como "Legacy IDE (1.8.X)" sin embargo, la instalación de la versión más moderna es igualmente funcional.
- Tenemos que asegurarnos que se instalen correctamente los controladores que nos sugiere el instalador. Estos controladores son necesarios para que nuestra computadora detecte correctamente las tarjetas conectadas por medio de los puertos COM (en Windows) ó tty (en Mac y Linux).
-
Tras instalar el IDE debemos acceder a la pestaña de preferencias.
- Al entrar en la ventana de preferencias debemos agregar la siguiente dirección al recuardo marcado como "Gestor de URLs Adicionales de Tarjetas:"
https://dl.espressif.com/dl/package_esp32_index.json
Una vez presionado el botón "OK" comenzará la descarga de las bibliotecas necesarias de Espressif para la compilación del código de Arduino a nuestra tarjeta ESP-32.
Posteriormente, únicamente será necesario subir a la tarjeta ESP-32 el código aquí proporcionado.
Para cualquier referencia posterior sobre cómo subir un programa a una tarjeta de desarrollo recomiendo este video.
Cómo usarlo
Aquí veremos cómo utilizar la aplicación del osciloscopio. En realidad existen tres aplicaciones presentadas en este proyecto, existe un generador de funciones sinusoidales, una aplicación que actúa como intermediario (o traductor) entre el puerto serial al que está conectado el ESP-32 y por último, una aplicación en la cual se grafican las señales leídas.
Graficador
La interfaz al abrir el programa llamado "graficador-multicanal-udp.jar" debería de ser como la siguiente aquí mostrada. En caso de no poder ver la ventana secundaria (En la que se muestra la escala X, escala Y etc) presione la tecla F1 de su teclado.
En esta aplicación es en donde podremos ver las señales que leamos con nuestro osciloscopio o las señales que simulemos con el generador de señales sinusoidales. Estas tres aplicaciones estarán conectadas por puertos UDP (un protocolo de transferencia de datos que tiene la propia computadora) por defecto, el programa para la graficación estará conectado a los puertos 8080 y 8081. Esto vale la pena tenerlo presente.
En este momento no podemos visualizar nada en esta aplicación ya que no estamos enviando ningún valor para que se grafique, para ello deberemos iniciar los otros programas.
Exploraremos más adelante cómo manipular la imagen que estamos viendo, por el momento nos enfocaremos en obtener una lectura de voltaje en pantalla.
Generador de función seno
Podemos arrancar entonces el programa llamado "generador-seno-udp.jar" y deberíamos ver una ventana como la siguiente:
Por defecto, esta aplicación siempre se abrirá con los valores ahí marcados, hagamos una breve revisión de qué significa cada uno de ellos:
- PUERTO -> Es el puerto UDP al cual se encuentra conectado (recordar que el graficador solo puede leer dos puertos, el 8080 y el 8081 por defecto).
- PERIODO -> Es el periodo de la señal sinusoidal que generaremos, este tiempo siempre estará en nanosegundos. El valor escrito por defecto (1000_000_000) es equivalente a un segundo. Al momento de introducir números se puede usar el guión bajo "_" como separador.
- AMPLITUD -> Será la amplitud de la señal que generaremos en Volts.
- DESFASE -> La señal sinusoidal aquí generada se genera a partir de la función matemática Math.sin() y usamos el argumento usado es siempre el tiempo en nanosegundos. Esto significa que si abrimos dos generadores de sinusoidales y a los dos les damos el mismo desfase, estarán en fase entre ellas.
NOTA: Para aplicar los cambios de los valores es importante SIEMPRE presionar la tecla Enter en las celdas que hayan sido modificadas.
Podemos comenzar encendiendo nuestro generador de senos mientras tengamos ya abierto el graficador. Deberíamos de obtener algo como lo siguiente:
Parecería que únicamente apareció una línea amarilla, sin embargo, si observamos con detalle podremos ver que esa línea está en movimiento. Podemos cambiar la amplitud de la señal que generamos seleccionando la celda de AMPLITUD y escribiendo un número, como 100. No es necesario pero se puede agregar el punto decimal. Presionamos Enter y deberíamos observar los cambios en el graficador.
Podríamos agregar un segundo generador de señales sinusoidales y manipular el periodo, el desfase y la amplitud a voluntad. Intentémoslo:
Podemos observar que cláramente hay un error en la representación de las señales que estamos mandando. Este error es muy común y por ello es importante tener cuidado; en este caso se están mandando las lecturas de ambos generadores de señal por medio de los puertos 8080. Tenemos que cambiar uno de los dos para que se envíen por medio de otro puerto (el 8081). Así, si cambiamos el puerto de uno de los dos generadores obtendremos lo siguiente:
Tal y como esperábamos.
Podemos cerrar las aplicaciones de generación de señal en cualquier momento y no habrá ningún problema en cuanto al graficador de señales se refiere. Podríamos abrir y cerrar tantos generadores de señales como quisiéramos. Igualmente, si llegáramos a cerrar el graficador por error, podríamos volverlo a abrir e inmediatamente comenzaría a graficar los valores en caso de que un generador de señales ya se encontrara encendido.
Traductor Monitor Serial - UDP
Este programa tiene la función de actuar como intermediario entre la conexión de la ESP-32 con la computadora y así poder manipular los valores brindados por la tarjeta de una manera útil.
Podemos arrancar la aplicación al igual que con las otras abriendo el archivo "traductor-serial-udp.jar" con el cual deberíamos ver una ventanita de este estilo:
En esta ventanita tenemos más opciones que en el generador de funciones seno ya que hay una serie de cosas adicionales que tenemos que contemplar al momento de traducir los valores que nos suministra la ESP-32. De manera general, si el circuito que usted armó es exactamente igual al propuesto en el capítulo anterior, lo único que deberá cambiar es el valor del PUERTO cuando quiera conectar dos señales (sinusoidales o incluso dos traductores de la ESP-32) tal y como lo hicimos en el ejemplo pasado. Además, también deberá cambiar la celda que está marcada como COMPORT. Esta celda sirve para seleccionar el puerto serial al que queremos que nuestro programa se conecte.
Si quisiera utilizar un arreglo diferente de resistores (por ejemplo, para tener una menor atenuación de los voltajes o tener una mayor atenuación para lectura de voltaje más elevados a los 50V de diseño, tendrá que registrar los valores de los resistores tal y como se indica en la sección del diseño avanzado) Aquí
Para el grueso de los usuarios, se espera que se pueda conectar directamente al conectar nuestro dispositivo armado con los resistores y la tarjeta ESP-32.
Comencemos entonces con un ejemplo. Se sugiere ya tener conectada la tarjeta ESP-32 al momento de arrancar el programa traductor, sin embargo, no es indispensable.
Es necesario picar la celda marcada con COMPORT. Presionar esta celda abrirá una pequeña ventana donde estarán listados todos los dispositivos conectados por medio de comunicación serial. El número a la izquierda es el índice que ocupan, el nombre a la derecha es el nombre que les asigna el sistema operativo. De no abrirse con el primer clic, vuelva a presionar la celda.
Debería abrirse una pequeña ventana como ésta:
Identifique el índice del dispositivo que desea conectar. En Windows generalmente buscaremos conectarnos al dispositivo COM0, sin embargo, esto puede cambiar de equipo en equipo. En el caso de Mac y Linux buscaremos conectarnos al dispositivo de nombre tty.usbserial-####. En la presentación realizada aquí con Mac nos conectaremos entonces al índice 7. En el ejemplo en Windows nos conectaremos al índice 0.
Cambiamos el valor en COMPORT y presionamos Enter. Nota: Podemos estar seguros de que el cambio se realizó si al presionar Enter nos cambia a la celda siguiente.
Tras asegurarnos que la celda COMPORT y la celda PUERTO estén en los valores que deseamos podemos presionar el botón de "Encender". En el caso de esta presentación, dado que no se está aplicando ningún voltaje y la conexión con el COMPORT fue exitosa, podemos observar la línea amarilla de voltaje en el voltaje de 0V.
Podemos asegurarnos que ya existe lectura de voltaje si tocamos las conexiones de la ESP-32 con la mano y ocasionamos ruido. No es la mejor manera de asegurarnos de la conexión pero es una manera rápida y efectiva en campo.
Error de conexión
Al presionar el botón de encendido o actualizar la celda COMPORT existe la posibilidad de que la conexión no se realice de manera correcta. Si esto llegara a pasar aparecerá una ventanita como la siguiente:
Cuando esto suceda, cierre la ventana, actualice el puerto COMPORT y vuelva a presionar el botón de "Apagado" para encender.
Controles del graficador
Todos los controles son accesibles desde el teclado, sin embargo, se pueden realizar los mismos cambios desde la interfaz gráfica al presionar F1.
Las unidades son un poco extrañas al momento de trabajar en la GUI por lo que requieren un poco de explicación. Al presionar F1 obtendremos esta pequeña interfaz:
Las unidades en la sección de Escala X son los nanosegundos representados en cada pixel en la escala original de la ventana. Las unidades en la sección de Escala Y son los voltios representados en cada pixel en la escala original de la ventana en el eje Y. Al modificar este valor también se modifica el valor de voltaje en el eje Y al realizar graficaciones en modo Lissajous.
El desplazamiento en X y Y están marcados con un % ya que se pueden realizar desplazamientos de 1% de la ventana en los ejes izquierda-derecha o arriba-abajo. Al presionar el modo Lissajous se entra en modo de graficación de figuras Lissajous.
Para realizar los cambios en las celdas es necesario presionar el botón Enter al igual que en el resto de las aplicaciones.
Atajos de teclado (RECOMENDADOS)
Los mismos cambios que se pueden realizar en la interfaz de usuario se pueden realizar desde el teclado. Además, se puede pausar la graficación desde el teclado.
Incremento | Decremento | |
---|---|---|
Escala Y | Q | A |
Escala X | W | S |
Despl. X | Z | X |
%Despl. Y1 | U | J |
%Despl. Y2 | I | K |
Pausa | Barra | espaciadora |
Modo Lissajous | L | L |
Los atajos tienen un poquito de lógica una vez uno empieza a usarlos. Es más rápida la navegación por medio de teclado por lo que es recomendable para todos los usuarios.
NOTA: El uso de la interfaz gráfica o del teclado ambos realizarán los cambios aún cuando la graficación esté pausada. Para realizar el análisis de señales con cuidado se recomienda presionar la barra espaciadora para pausar la lectura de señales y posteriormente manipularlas ya sea con teclado o con la interfaz gráfica.
NOTA: La ventana de graficación se puede escalar a placer, el tamaño de las etiquetas escalará de acuerdo al tamaño de la ventana en el eje horizontal.
Decisiones de diseño
En este capítulo le dedicaré una sección a las decisiones finales del diseño. Tras explorar múltiples alternativas de diseño (tratadas en el capítulo anterior) se estableció que la mejor manera de construir el osciloscopio sería por medio de:
Dado que únicamente se tratará el diseño de la implementación final, no entraré demasiado en detalle de los diseños descartados ni de los valores concretos de resistores, voltajes etc. todo esto se trata en el capítulo de usuario avanzado donde se explican patrones de diseño así como fallas encontradas durante el proceso.
¡¡Atención, esta sección explica a detalle la lógica utilizada para el diseño del osciloscopio, se recomienda un conocimiento intermedio de electrónica!!
Así, en cada capítulo proseguiré a explicar qué hace cada parte del osciloscopio, intentando transferir la lógica detrás de cada decisión que sí se quedó en el diseño final, no el comportamiento de manera tan detallada o minuciosa.
Ideas de diseño
El diseño de un nuevo dispositivo se parece a la exploración de un nuevo mar, se parece en el sentido de que a pesar de que hay muchas manera de llegar a nuestro destino, habrá unas con el mar más quebrado, unas que requerirán de un barco más equipado, unas que requerirán de una tripulación más grande.
Siguiendo esta analogía, yo soy un barquero en una chalupa que le promete llegar al otro lado del Atlántico por solo 300 pesos. Quizá resulte difícil de creer pero quizá con eso me haya ganado su atención con este pequeño cuento. Además, no lo vea como comprar un viaje, yo aquí únicamente le estoy dando un panfleto de viajes "¡Llegue a América con 300mxn!".
En este capítulo se tratarán las múltiples ideas que se exploraron antes de comenzar con la exploración de la última iteración del proyecto. Por lo mismo, considero que no es demasiado importante profundizar en el funcionamiento propuesto al momento de proponer estas implementaciones, sin embargo, creo que merecen su mención.
A pesar de que muchos de estos fallos tienen una potencial solución relativamente sencilla, estas soluciones generalmente exceden el presupuesto límite, la facilidad de acceso para estudiantes o son un riesgo para el dispositivo a manos de un usuario inexperto. Esta lista está aquí más bien para dar idea del proceso de diseño que para descartar las ideas como inviables.
IDE Arduino
El IDE de Arduino es para muchos el primer contacto que tienen con el desarrollo de herramental lógico para tarjetas integradas. Ya sea por medio de las tarjetas de Arduino, las de la serie ESP o circuitos integrados de otros fabricantes, el IDE Arduino se ha convertido en una especie de herramienta estándar de la industria por su facilidad de uso. En este IDE podemos encontrar ciertas herramientas ya incluidas para realizar la lectura del monitor serial.
El IDE Arduino está programado en Java en buena medida por su compatibilidad entre diferentes plataformas y sistemas operativos. Al IDE también se le pueden agregar diferentes complementos para hacerlo "más completo" según el caso de uso que uno le esté dando. Consideré por un tiempo que quizá aprovechar gran difusión de el IDE Arduino sería benéfico al momento de realizar este proyecto, sin embargo, por las cuestiones aquí planteadas abandoné esa idea.
- Limitantes de diseño
- Dependencia de código exterior (Arduino) (La creación de este proyecto coincidió con el cambio del IDE Arduino a la versión 2.0)
- Inestabilidad del IDE al momento de cambiar puertos (en especial en computadoras con sistema operativo (SO) Mac y Linux)
- Propuestas de valor:
- Java como lenguaje de programación por su compatibilidad entre SOs
- Uso del puerto serial como medio de comunicación ESP-32 -> Computadora
Programación en C++ con biblioteca olcPixelGameEngine.h
El segundo lenguaje de programación contemplado para la realización de este proyecto fue C++ haciendo uso de diferentes bibliotecas, en el caso de C++ (a diferencia de Java) no se incluye una manera directa de crear ventanas y graficar sobre de ellas por lo que es un poco más complicado realizar interfaces gráficas con aceleración por medio de la tarjeta gráfica (GPU).
Consideré que olcPixelGameEngine (documentación en inglés) era una opción viable para conseguir la graficación de las lecturas del osciloscopio por ser suficientemente amable al momento de graficar a altas velocidades y proporcionar un ciclo de graficación análogo al de la programación de un videojuego. Esto permitía que a pesar de que todo se graficara lento, uno podría mantener una imagen constante con problemas menores.
Hubo ciertas consideraciones que surgieron al momento de empezar a programar con C++, no existe una biblioteca única en C++ para el menejo de los puertos seriales por lo que la programación cambiaría bastante según qué biblioteca eligiera. En su momento había utilizado JSerialComm ya que era la más referenciada en el trabajo en Java, sin embargo, no había ningún análogo "estándar de industria" en C++.
Entre los problemas surgidos por el desconocimiento de C++ así como las ventajas que traía Java a la mesa surgieron las siguientes conclusiones:
- Propuestas de valor:
- Java como lenguaje de programación por la biblioteca JSerialComm
- Java como lenguaje de programación por la biblioteca Swing de interfaces gráficas
- Planteamiento de la graficación, ya fuera en Java, C++ o cualquier otro lenguaje de programación como un problema análogo al dibujo de entidades en la pantalla en un video juego.
Uso de ADC (Convertidor Analógico Digital) externo
Esta idea surgió y rápidamente desapareció ya que como se planteo en el capítulo de introducción. El uso de un dispositivo ADC externo podía haber resultado benéfico para conseguir mayores velocidades de muestreo, sin embargo, por restricciones de precio y de accesibilidad a los estudiantes esta propuesta fue rápidamente descartada.
- Propuestas de valor:
- Mientras menos partes tenga el dispositivo armado al final será más barato, fácil de armar y fácil de reutilizar para los estudiantes por lo que partes especializadas como un ADC externo (ADC Flash) quedaron descartadas.
Uso de tecnología Bluetooth
La tarjeta ESP-32 así como varias de las otras consideradas tienen capacidades de Bluetooth así como de Wi-Fi, sin embargo, dado que el máximo baudaje por medio de Bluetooth 4.0 es de 115200 baudios. Este protocolo serial, se traduce a 14'400 Bytes por segundo, cada una de las mediciones, por ser de 12 bits de resolución necesitaríamos 16 bits por lectura yendo desde la ESP-32 hacia la computadora. A pesar de que en papel esto debería de darnos aproximadamente un 1kHz en lecturas, la realidad es que este tiempo únicamente considera el tiempo que le toma al protocolo RXTX en la ESP-32. Dada la naturaleza de la tarjeta (cuyo lenguaje ensamblador no está bien documentado) estamos trabajando con el compilador de Arduino como una caja negra y por tanto no tenemos demasiado control con respecto a esto.
De todas formas, fijar la tasa de baudios a la mayor posible (en nuestro caso 1'000'000 de baudios) permitió aumentar sustancialmente la frecuencia de muestreo. Para mantener esta tasa de baudios fue necesario trabajar siempre con un cable USB-MicroUSB. Así también ahorrando la necesidad de usar alimentación con otros dispositivos.
- Propuestas de valor:
- A pesar de que hay procesos que no interfieren de manera directa con el ADC sí mantienen ocupado al procesador del ESP-32 por lo que tener un programa que tome menos tiempo en cualquier proceso es indispensable.
- Evitar usar condicionales, código complejo, funciones, bibliotecas externas etc. ayudaría a mantener una mayor velocidad de muestreo. RXTX via cable resultó siempre ser la mejor opción en términos de practicidad-velocidad.
Uso de baterías y alimentación externa
Simultáneamente a la exploración de la tecnología Bluetooth se consideró que el dispositivo osciloscopio podría ser un aparato con independencia energética de la computadora o teléfono móvil al que se encontrara conectado. Esto signficaría el uso de una batería para manterlo encendido y operando. Dado que el funcionamiento de las tarjetas ESP-32 es con voltajes de 3.3V necesitaríamos dos baterías de Ion Litio (Lion) para tenerlo funcionando de manera segura. Además, el tener baterías en serie nos facilitaría el uso de voltajes positivos y negativos con respecto a un punto de referencia (i.e. -2.2V a 2.2V con respecto al punto entre ambas baterías). La realidad es que el manejor de las baterías de litio resultó ser más conflictivo de lo que parecía inicialmente si uno decidía no usar dispositivos para su manejos.
Análisis muy posteriores (realizados por el laboratorio de Robótica en el edificio J) junto con información anecdótica por parte del profesor Yukihiro Minami dieron sustento y completitud a la hipótesis del fallo observado en su momento.
- Al armar el dispositivo para su uso con baterías, al momento de emparejar tierras entre las baterías de Ion-Litio y la computadora en la cual se realizaría la visualización de los datos de RXTX rápidamente se quemaba la tarjeta ESP-32 que se encontrara conectada.
La información posterior reveló que en efecto era muy complicado obtener un comportamiento confiable de las baterías de Ion-Litio sin utilizar cuatro tipos de de circuito adicional:
- Desconección automática a bajo voltaje
- Circuito controlador de carga
- Circuito controlador de descarga
Sin estos dispositivos o un (qph) (dispositivo que contiene a los tres) sería casi imposible utilizar baterías de Ion-Litio de forma segura. Es por eso que quedaron descartadas.
- Propuestas de valor:
- Todos los escenarios en los que es necesario emparejar voltaje pueden resultar peligrosos para el circuito de medición por lo que es ideal eliminarlos o evitarlos
- A pesar de que las baterías de Ion-Litio no son tan nuevas, intentar manejar nuevas tecnologías como ésta ahorrando la seguridad de los circuitos de carga/descarga/desconección es mucho más riesgoso de lo que uno podría pensar en primera instancia. Se decidió omitir el uso de toda esta tecnología por precio y seguridad del circuito ya que está diseñado por gente que falló para gente que igualmente podría fallar en obtener los resultados esperados.
Uso de aplificadores operacionales
Durante la mayor parte del desarrollo de este circuito se contempló que sería imposible evitar el uso de amplificadores operacionales (a veces llamados "operacionales") para dar ganancia, reducción, filtrado y demás manipulaciones que se quisieran aplicar a las señales eléctricas leídas por el osciloscopio. A pesar de que la mayoría de los dispositivos del mundo real sí usan amplificadores operacionales para la manipulación de las señales que utilizan, en nuestro caso, una de las decisiones más difíciles de tomar fue el abandono total de los amplificadores para hacer la manipulación de la información de la señal completamente en herramental lógico (software).
Se llegó a esta decisión cuando se abandonó la idea de utilizar baterías externas. Esto nos llevó a únicamente poder trabajar con un intervalo de 5V para alimentar los amplificadores operacionales y con este voltaje poder invertir, amplificar, reducir o seguir voltajes. No pudimos lograr ninguno de estos comportamientos (además de la reducción) usando amplificadores de la serie TTL (los que se suelen usar en las otras prácticas de la Facultad) esto significaría tener que comprar dispositivos más raros de bajo voltaje haciendo el proyecto más difícil de conseguir y económicamente menos viable de lo que se deseaba.
- Propuestas de valor:
- Se decidió eliminar el uso de los dispositivos más usados en la industria para manejo de señales por cuestión de precios. Esto nos dejó únicamente con la posibilidad de usar resistores, condensadores, diodos y cable. La solución a encontrar tendría que ser muy sencilla o de plano no existir.
Uso de condensadores para separar señales
Entre las propuestas que tiene un osciloscopio siempre se encuentra la separación de las señales de corriente alterna (CA) y corriente directa (CD). Esto puede resultar muy útil al momento de visualizar valores así como dar información que no es necesariamente fácil de leer únicamente a partir de gráficas pero sí resulta fácil de interpretar para una calculadora que conoce la información y la puede promediar o calcular.
Por medio de condensadores hubiera sido fácil separar la señal leída en dos diferentes señales, una señal pasa-bajas en la que únicamente se mediría el promedio de la señal introducida (esto nos daría inmediatamente el componente de CD) mientras que por medio de un condensador de "bypass" se podía haber desacoplado el componente de directa y únicamente medir la oscilación de voltaje después de ese condensador de desacomple. Esta idea se abandonó cuando se ponderaron las ventajas de usar condensadores contra el hecho de no usarlos.
Ventajas:
- Facíl desacople de señales
- Garantía de protección tras el condensador de desacople
- Lectura inmediata del voltaje de CD
- Lectura inmediata de voltaje de CA con respecto a la referencia
Desventajas:
- Estados transitorios (inexactitudes)
- Tiempo de transitorio (carga y descarga) a menos que se tuviera una señal estacionaria
- Desfase de señales de alta frecuencia en el tiempo
- Precio peligrosamente cercano al presupuesto máximo permisible
La realidad es que de haber querido armar un osciloscopio "mejor" hubiera sido posible dejarlos en el diseño final, sin embargo, se obviaron, dejándole el trabajo del análisis de las señales completamente al herramental lógico.
- Propuestas de valor:
- No es fácil trabajar con componentes no lineales. En algunas cosas resultan realmente convenientes pero al momento de hacer ajustes aparentemente menores en el resto del diseño es muy fácil obtener comportamientos de partes "olvidadas" que antes funcionaban ya de la manera esperada. Poder mandar todo a una capa abstracta (herramental lógico) hace mucho más fáciles las cosas.
Uso de botones y controles externos
La propuesta inicial de osciloscopio contemplaba la posibilidad de modular resistores así como mandar comandos a la ESP-32 por medio de botones o interruptores. Con el abandono de los condensadores también se abandonó la idea de utilizar resistores modulares (potenciómetros) ya que tenerlos en el dispositivo hubiera significado un mayor nivel de desconocimiento con respecto a los resistores reales que de tener resistores fijos con un error de fábrica.
Posteriormente se volvería bastante claro que a pesar de tener un error de fábrica, dado que los fenómenos observados en los resistores son siempre lineales en nuestro rango de voltajes/corrientes esperado sería mayormente sencillo ajustar las lecturas con valores de voltaje de referencia y así tener valores más cercanos a la realidad pese a tener errores inherentes a la manufactura de nuestro dispositivo.
- Propuestas de valor:
- Dado que existe la posibilidad de usar un voltaje de referencia para la calibración de nuestro osciloscopio, es posible calibrar haciendo uso de un par de constantes linealmente dependientes del voltaje y la resistencia por lo que es posible calibrar todo por medio del herramental lógico.
Circuito
En esta sección se tratará muy brevemente el circuito final que se utilizó para acondicionar la señal leída.
Después de las muchas consideraciones que se tomaron en la sección anterior, ya que la gran mayoría de las cosas ideas terminaron en fallo, el diseño se fue reduciendo más y más hasta poder trabajar únicamente con resistores y la tarjeta de desarrollo ESP-32.
Entonces, todo el diseño del osciloscopio se vio reducido a una máquina lineal (la cual iremos descubriendo a lo largo de este capítulo) la cual consta únicamente de resistores y un convertidor analógico digital (ADC).
Esto quiere decir que tenemos una función lineal que relaciona el voltaje que queremos leer (previo al acondicionamiento) y el voltaje que va a leer la tarjeta ESP-32 (posterior al acondicionamiento). Esta función la podremos expresar como:
\[ V_{ESP-32}(V_{Real}) = V_{Real} * K_{Atenuación}\]
Lo que tendremos que hacer es diseñar un circuito tal que conozcamos la atenuación de nuestros resistores a priori además de poder aprovechar el rango de voltajes disponibles en el ADC de nuestro ESP-32. Esto sugiere que tendremos que hacer un centrado posterior a la atenuación de nuestra señal eléctrica:
\[ V_{ESP-32}(V_{Real}) = V_{Real} * K_{Atenuación} + C_{Ajuste}\]
Por conveniencia de diseño podemos solicitar en nuestro diseño que el voltaje mínimo sea el voltaje mínimo posible a leer con la ESP-32. De la misma manera, podemos solicitar que el voltaje máximo sea el voltaje máximo disponible para el ADC de la ESP-32.
\[ V_{ESP-32}(V_{Min}) = 0[V] = V_{Min} * K_{Atenuación} + C_{Ajuste} \] \[ V_{ESP-32}(V_{Max}) = 3.3[V] = V_{Max} * K_{Atenuación} + C_{Ajuste} \]
Por último, resulta más o menos obvio (pero podemos explicitarlo) que el voltaje central (el voltaje de 1.65[V]) será el voltaje en el cual estaremos leyendo 0[V] previos al acondicionamiento. Por eso lo llamaremos "real cero" o V_cero.
\[ V_{ESP-32}(V_{cero}) = 1.65[V] = V_{cero} * K_{Atenuación} + C_{Ajuste} \]
El volaje mínimo entonces será el voltaje de misma dimensión pero signo opuesto al voltaje máximo. Lo que nos garantizará que en el intervalo de operación para V_real (V_real siendo el voltaje previo al acondicionamiento) \[ V_{real} \in (V_{min}, V_{max})\] Así, tendremos valores siempre positivos entre 0[V] y 3.3[V] en la lectura del ESP-32 y así evitaremos darle voltajes superiores a 3.3[V] o inferiores a 0[V] con respecto a la tierra de la tarjeta o a la computadora a la que se encuentre conectada, (respetando así nuestro intervalo de operación seguro en todo momento).
Esto nos deja con la siguiente tarea de diseño: Diseñar un circuito con resistores tal que funcione como un circuito de atenuación de factor K, donde además podamos centrar el voltaje.
Solo por el bien de hacer una gráfica descriptiva en los ejes X, Y, con X siendo "Voltaje real" y Y siendo "Voltaje ESP-32", sugeriremos un Vmax de 20[V], un Vmin de -20[V] y un voltaje "cero" de 1.65[V].
Graficado en Desmos
Existen muchas muchas maneras de obtener un comportamiento semejante a este haciendo uso únicamente de resistores ya que hay una infinidad de arreglos que pueden proporcionar este comportamiento. En este caso en concreto, se decidió partir de un divisor de voltaje.
La idea fue:
- Supongamos que tenemos un arreglo de dos resistores en serie (el circuito llamado divisor de voltaje)
- Ese divisor de voltaje lo podemos diseñar de manera que queden exactamente en 1.65[V] en el nodo central.
- Si agregamos un tercer resistor a este nodo, podremos "desviar" el voltaje hacia valores más altos o más bajos. En este ejemplo, el voltaje cambia 1/3 del voltaje original ya que está conectado a tierra pero no es difícil de ver que si el nuevo resistor estuviera conectado a un voltaje igual al generador inicial el nuevo resultado sería 2/3 del voltaje original (en vez de 1/2, como cuando había solo una entrada y una salida).
-
Este resistor nuevo lo podemos llamar N. Si los dos resistores originales los llamamos R1 y R2 quedaría un nodo central conectado en paralelo a tres resistores, R1 con voltaje, R2 con tierra y N con un voltaje variable. En ese nodo compartido por R1, R2 y N podremos realizar mediciones con el ADC del ESP-32. Este nodo, será tan importante que lo podremos bautizar de una vez como el nodo "RN".
-
Pensemos que el voltaje variable dependiera no de nosotros sino del fenómeno que estamos intentando medir. Ya que todo nuestro modelado es lineal, sería bastante fácil saber qué voltaje está ocurriendo del lado opuesto de N. N es de alguna manera nuestro "termómetro de voltaje" ya que lo podemos colocar en otro lugar y obtendremos una medición. Esta medición ya será bastante interesante puesto que podremos determinar el voltaje de el dispositivo al que se conecte N con respecto al voltaje de tierra y el voltaje de operación de la ESP-32. Esto está bien PERO nosotros somos un poco más ambiciosos, estamos buscando medir el voltaje entre dos puntos, no importa cuáles sean. Esto quiere decir, no nos interesa realmente el voltaje del punto con respecto a la tarjeta ESP-32 sino, nos interesa la relación del voltaje entre dos puntos del mundo externo, no su relación con la tarjeta ESP-32.
Ahora, recordemos brevemente el divisor de voltaje, el divisor de voltaje divide \[ V = \frac{V1 - V2}{R1 - R2} \]
Supongamos que la manipulación del voltaje de N la realizáramos por medio de esa división de voltaje en un nuevo divisor de voltaje paralelo.
- Dado que nuestro sistema de alambrado está constituido de resistores que no cambian con el voltaje (o al menos eso queremos suponer) podremos obtener fácilmente el valor de (V1 - V2). Así, podremos saber cuál es el voltaje entre dos puntos. O bueno, ya casi. Hay que recordar que el valor de voltaje que estamos leyendo en todo momento es el voltaje en el nodo compartido por N, R1 y R2.
Ahora, si hiciéramos eso ¿qué valores leeríamos? Todos los valores leídos serían con respecto a los valores extremos de operación (en nuestro caso 0[V] y 3.3[V]).
Vale la pena mencionar, al hacer esto, los valores de 1k ya no funcionan como quisiéramos ya que la introducción del resistor N hizo que ya no fuera un divisor de voltaje invariable además que desconocemos qué voltajes se van a introducir en los resistores S1 y S2.
Aquí es donde, si uno no presta suficiente atención o realiza las suficientes pruebas es muy fácil dejar que un error pase desapercibido. Hasta el momento no hemos cometido ningún error garrafal ¿o sí?
Hagamos un rápido resumen:
- Pensamos en el divisor de voltaje de dos resistores como nuestra base. (resistores R1, R2)
- Le agregamos un tercer resistor por medio del cual introduciríamos una variación en el voltaje del nodo central del divisor de voltaje. (resistor N)
- Agregamos un segundo divisor de voltaje por medio del cual obtendríamos la diferencia de voltajes entre las dos terminales de medición del osciloscopio (resistores S1 y S2). Con este divisor de voltaje obtendríamos un gradiente de voltaje entre S1 y S2 con el cual afecteríamos de manera predecible el voltaje en el nodo central de R1, R2 y N.
Aquí es donde algo se nos está pasando, un error que tomó mucho rato encontrar y resolver.
El nodo R1, R2, N no está desacoplado del nodo S1, S2, N. Este es el verdadero problema. El hecho de que no estén desacoplados puede parecer una nimiedad, sin embargo, este pequeño detalle destruye nuestro diseño. (O al menos nos obliga a replantear algunas cosas).
Llamemos a los nodos S1, S2, N el nodo SN y a R1, R2, N el nodo RN.
Supongamos
- El voltaje de tierra se encuentra en el resistor R2.
- El voltaje de 3.3V se encuentra en el resistor R1.
- Algún voltaje (solo por esta vez, pensemos que 3.3V) se encuentra en el resistor S1
- Algún voltaje (solo por esta vez, pensemos que 0V) se encuentra en el resistor S2.
Esperaríamos que el voltaje en el nodo SN se encuentre en alguna vecindad que nuestro osciloscopio pueda manejar. Este voltaje en SN generaría una desviación de voltaje en el nodo RN y podríamos calcular el voltaje en el nodo SN y a partir de éste podríamos calcular el voltaje en S1 y S2 (los dos verdadores valores de interés). La realidad es que en este escenario (0V y 3.3V) no tendríamos ningún cambio de voltaje en el nodo RN. Además los resultados obtenidos serían un sinsentido, ¿qué voltaje se mostraría si en S1 tuviéramos un voltaje altísimo y en S2 un voltaje negativo de un enorme valor absoluto? Sería impredecible y sería dependiente del valor de N. Si N fuera muy grande podríamos casi lograr un desacoplo, si N fuera muy chica la influencia del voltaje SN en el voltaje RN sería enorme. Pensemos también en el escenario ¿y si tanto el voltaje de S1 como el voltaje de S2 fueran altísimos? Quizá no obtendríamos una real diferencia de voltaje en el divisor SN pero sí veríamos una influencia muy grande en el nodo RN.
Está mal, falla por todos lados y solo funcionaría nuestro circuito en un puñado muy muy selecto de escenarios. Es por eso que tenemos que cambiar nuestro modelo.
Regresemos al paso en que agregamos N.
Venga, una vez más, paso 4. Esta vez hagamos una consideración adicional; vamos a medir el voltaje de un solo punto, no la diferencia de voltajes.
- El resistor nuevo lo podemos llamar N, por medio de este resistor podremos introducir un voltaje y por medio de este resistor conoceremos el valor de voltaje en UN SOLO PUNTO con respecto a nuestro nodo RN.
Este sistema sabemos que funciona, sabemos que no estamos cometiendo ningún error hasta este momento: Tres resistores, tres fuentes de voltaje, conocemos el valor de dos fuentes, conocemos el valor de todos los resistores, podemos caracterizar perféctamente el valor del voltaje introducido a N si conocemos el valor de voltaje en RN.
- Llegamos una vez más al punto en que necesitamos saber el voltaje en dos puntos, no nada más en uno ya que todo osciloscopio necesita un voltaje para la terminal negativa y uno para la terminal positiva. Deje de leer en este punto si desea darle vueltas un poco por su propia cuenta.
Nota: Me intriga saber qué soluciones se le pueden haber ocurrido, lector. Ya llegaremos a la parte de diseño avanzado donde quizá pueda ponerlas en marcha.
- Agregamos un segundo arreglo de tres resistores, R1, R2 y N, una vez más, una medición más. Entonces, obtendremos el siguiente arreglo de resistores:
Así ya tendremos el arreglo A y el arreglo B. Con el arreglo A realizaremos la medición de un voltaje con una punta de terminal (podemos definir que el arreglo A lleve a la terminal roja "positiva") y con la otra terminal podemos realizar la otra medición (el arreglo B lleva a la terminal negra "negativa").
- Aquí termina la perte del diseño del circuito, realizaremos la medición del nodo RN_A y del nodo RN_B de manera simultánea con el ESP-32 ya que esta tarjeta cuenta con dos ADCs. Esta es una más de las razones por las cuales resulta tan conveniente esta tarjeta.
Leídos los dos voltajes de manera individual podremos realizar una resta entre ellos y hacer todas las manipulaciones posteriores por medio de herramental lógico (software).
Conclusión
En la parte de usuario avanzado explicaré a más detalle cómo calcular los valores de los resistores a usar, cómo determinar los voltajes máximos seguros así como explorar los múltiples diseños que se llevaron antes de llegar a este último arreglo que resultó aparentemente tan sencillo al final.
Del diseño podemos concluir que muchas veces lo mejor es delegar la responsabilidad de realizar operaciones lógicas a la programación y a las capas más abstractas del proyecto. Es por esto que siempre se intenta delegar responsabilidad a "una capa superior". Lo mismo ocurrirá cuando trabajemos con el ESP-32, le delegaremos responsabilidades a aplicación de Java ya que ahí tendremos todavía más holgura en cuanto a capacidad de procesamiento.
Usuario Avanzado: diseño del circuito
ESP-32
La tarjeta de desarollo ESP-32 fue la que quedó como la mejor alternativa a en cuanto a tarjetas con un circuito integrado se refiere. Existen alternativas que son ligeramente más sencillas de conseguir por precios no muy diferentes, sin embargo, la ESP-32 presenta una serie de ventajas que estas alternativas no.
Las tarjetas de Arduino, la Nano y la Uno así como la ESP-8266 fueron también contempladas.
Ninguna de estas tarjetas es tan mala como para quedar inmediatamente descartada pero hay una razón final por la que la ESP-32 quedó como la única tarjeta recomendada para elaborar este osciloscopio (aunque también se podría armar el osciloscopio con cualquierade estas otras tarjetas).
RECOMENDACIÓN
- La tarjeta ESP-32 tiene una velocidad de reloj de 240MHz, un ADC con resolución de 12 bits (0->4095) y un precio de 160 mxn SandoRobotics.
Las demás tarjetas pueden hacer de un buen sustituto de uno ya contar con alguna de estas tarjetas por algún proyecto previo, sin embargo, de uno comprar uno nuevo, la tarjeta ESP-32 es la que se recomendará en este texto.
ALTERNATIVAS
-
Arduino Nano: Las dimensiones y los casos de uso son semejantes a las de la ESP-32, el ADC igualmente tiene una resolución de 12-bits, la única diferencia real para el usuario está en la velocidad de reloj. La tarjeta de Arduino Nano tiene una velocidad de reloj de 80MHz mientras que la ESP-32 tiene 240MHz por cada núcleo. No es una mala tarjeta ni una mala alternativa pero de ser posible, es mejor adquirir la ESP-32 por el mismo precio. SandoRobotics
-
ESP-8266: Es la tarjeta "anterior" en diseño a la ESP-32, en general también es una buena alternativa. Tiene una velocidad de 80MHz, lo que la sitúa en el mismo lugar que la Arduino Nano, sin embargo, su resolución de ADC es de 10 bits en vez de 12 bits. Esto nos daría un cuarto de los valores posibles a adquirir de voltaje si la comparáramos con la ESP-32 o la Arduino Nano, es por eso que, aunque no es mala tarjeta en caso de emergencias, no la podemos recomendar tanto como las otras dos. SandoRobotics
-
Arduino Uno: De las tres alternativas, esta es la que menos recomendamos ya que tiene una velocidad de reloj de 16MHz, una resolución de 10 bits (y solo en algunos modelos dependiendod el fabricante y la revisión de 12 bits) además de ser por mucho la más cara de las tres tarjetas alternativas. Es por eso que no la podemos recomendar como las otras. Más bien es un "último recurso".
Consideraciones
Para seguir este proceso de diseño se espera un conocimiento de programación moderado así como previa experiencia trabajando con tarjetas de desarollo, haber armado este circuito ya es suficiente fuente de conocimiento.
Dado que el objetivo de nuestro dispositivo es conseguir la mayor cantidad de mediciones por segundo, tenemos que tener un código optimizado para ciclarse lo más rápido posible, sin espacio a repeticiones e intentar pre-calcular la mayor cantidad de valores sin dejar que el compilador de Arduino haga magia detrás.
Se consideró el IDE de Arduino como la herramienta correcta para realizar este proyecto por la facilidad de instalación, compatilibidad en múltiples plataformas y facilidad de uso.
La tarjeta ESP-32 necesita de una biblioteca externa para su compilación, sin embargo, dado que existen mucho fabricantes de tarjetas de desarrollo ESP32-WROOM así como diferentes submodelos y revisiones intentaremos también apegarnos lo más posible a los comandos más utilizados del lenguaje de programación Arduino/C.
Se puede programar la tarjeta haciendo uso de diferentes compiladores así como de su lenguaje ensamblador, sin embargo, como ya se dijo, por facilidad de programación así como la implementación en general esto se tratará hasta el capítulo de diseño para usuario avanzado.
Diseño
Herramientas de depuración
El IDE de Arduino es ya suficientemente amable en el sentido de que ya incluye un visor para el monitor serial. El monitor serial es un canal de comunicación que existe para muchos dispositivos por medio del cual se envían señales (bytes de información) desde un dispositivo externo hacia la computadora. Estos bytes se envían uno tras otro (en serie) de ahí el nombre "serial". En el monitor serial de Arduino podemos visualizar texto así como el texto interpretado como un número (i.e. un String a un Entero).
Ojo, no podemos interpretar diréctamente el valor de los bytes enviados en el IDE de Arduino sino que necesitaremos una herramienta adicional para visualizar los valores que se envíen. Existen muchas herramientas por medio de las cuales se pueden visualizar los valores entrantes en el monitor serial así como muchos otros que permiten la comunicación en ambos sentidos.
ATENCIÓN: Los siguientes programas utilizados son únicamente una recomendación, ambas aplicaciones las he usado sin ningún problema aparente en cuanto seguridad. Mi recomendación es únicamente basada en la experiencia.
Para Linux y Windows, por mucho la mejor herramienta que encontré fue HTerm por su estabilidad y facilidad de visualización.
Para Mac CoolTerm fue la mejor alternativa.
Código
Todos los programas de Arduino comienzan con la plantilla siguiente
void setup() {
}
void loop() {
}
En el espacio de código previo el método "setup" generalmente se usa para la definición de variables globales y constantes (las cuales usaremos bastante), en la sección de "setup" irá el código necesario para inicializar nuestras variables, establecer funcionalidad de los pines de nuestra ESP-32 y demás configuraciones iniciales.
En "loop" estará el grueso de nuestro código, aquí es donde tendremos que tener suma precaución al momento de programar ya que este es el código que se ejecutará de manera cíclica una y otra vez.
Hay que recordar, la ESP-32 únicamente la usaremos como un medio de comunicación entre los convertidores analógico-digitales (ADC) a la computadora (en la aplicación de Java). Es por esto que en realidad el código será bastante conciso.
Un pseudocódigo más o menos funcional que represente lo que buscamos hacer sería:
int lectura_ADC_A
int lectura_ADC_B
int pin_ADC_A
int pin_ADC_B
int resta_ADC_AB
inicializa(){
//De este valor dependerá la velocidad de operación de la ESP32
configurar_reloj_ESP32
//De este valor dependerá nuestra velocidad de envío
configurar_tasa_baudios_monitor_serial(máximo_valor_estable)
//De este valor dependerá nuestra calidad de lectura
configurar_resolución_ADC(máximo_valor_estable)
}
ciclo(){
//Leemos los valores de las terminal positiva (A) y la terminal negativa (B)
lectura_ADC_A = leer(pin_ADC_A)
lectura_ADC_B = leer(pin_ADC_B)
//Al valor de la terminal positiva restarle el valor de la terminal negativa
resta_ADC_AB = lectura_ADC_A - lectura_ADC_B
//Mandamos el valor a la computadora
mandar_serial(resta_ADC_AB)
}
Este pseudocódigo describe mayormente el funcionamiento del código final. En realidad, este fue el primer código que se utilizó para intentar hacer funcionar el osciloscopio. Hay ciertos detalles que hacen que sea ineficiente, lento, variable y poco predecible. Es por eso que se tuvieron que realizar bastantes cambios a este código.
El osciloscopio con este código puede obtener aproximadamente doscientas muestras por segundo pero es muy variable y poco confiable. Es por ello que se requirieron de múltiples optimizaciones en el código para lograr un desempeño mejor del dispositivo. Ya con las mejoras en este código se pueden lograr mucho mayores frecuencias de muestreo. En las últimas pruebas realizadas antes de la elaboración de este manual se lograron velocidades de muestreo de 8kHz; lo que significa que podremos obtener un muestreo más o menos bueno de señales de 4kHz o señales de frecuencia inferior. En realidad, para tener una señal descrita por suficientes puntos como para poderla visualizar correctamente (unos 4 o 5 puntos por cada ciclo) se recomineda leer señales de hasta 1kHz. Sin embargo, en teoría se podrían interpretar señales de hasta 4kHz.
Desafortunadamente, las optimizaciones que se realizaron en este rubro del diseño superan muy rápidamente el nivel básico de usuario por lo que en realidad se recomienda que no se manipule el código a menos que uno realmente sepa lo que está haciendo.
Usuario avanzado: Diseño código Arduino
Diseño interfaz usuario
Como se mencionó en la sección anterior existen varias aplicaciones que se pueden utilizar para leer los valores del monitor serial. En realidad, dado que hemos programado la ESP-32 de la manera más eficiente posible, nos hemos quedado sin ninguna forma de interactuar con ella. Es por ello que tendremos que realizar toda la manipulación de las señales que envía por medio de otra aplicación que se ejecutará en una computadora personal.
Lectura de los valores del monitor serial
En los sistemas operativos más utilizados el día de hoy (Window, Mac, Linux y Android) es bastante fácil manipular los puertos seriales por medio del propio sistema operativo por lo que no tenemos que hacer una implementación de cero. Dicho esto, es muy latosa la implementación según el lenguaje de programación ya que a pesar de que es sencillo, habría que realizar la implementación distinta para cada sistema operativo de manera diferente. En este sentido es muy amable el uso de Java y la biblioteca que usa el IDE de Arduino para manipular el monitor serial: JSerialComm
Esta será la única biblioteca externa que usaremos para la implementación de nuestro osciloscopio.
La mejor solución que encontré para el diseño, puesto en pseudocódigo una vez más:
//Creamos una lista o un arreglo grande en el cual guardaremos los voltajes
lista<entero> lista_lecturas_adc
lista<flotante> lista_voltajes
inicialización(){
//Abrimos el puerto serial y lo configuramos para poder
//leer los valores que nos está mandando desde la ESP-32
abrir_puerto_lectura()
configurar_puerto(tasa_baudios, bytes_por_palabra)
abrir_flujo_de_bytes()
}
ciclo(){
//leemos los valores disponibles en el puerto serial
//y los agregamos a nuestra lista de valores en el ADC
lista_lecturas_adc.agrega(leer_bytes_puerto_serial())
//Convertimos los valores del ADC (en los cuales habrá enteros entre 0->4095)
//y los convertimos en valores de voltaje según el arreglo de resistores diseñamos
//en la parte del diseño del circuito
lista_voltajes = convertir_valores_ADC_en_Voltajes(lista_lecturas_adc)
//Graficamos los valores de voltaje leídos en una interfaz gráfica (por ejemplo una ventana)
graficar(lista_voltajes)
//Vaciamos ambas listas de voltaje para repetir el ciclo con una lista vacía
//Y así "olvidar" los valores previos y graficar únicamente los nuevos valores leídos
vaciar(lista_lecturas_adc)
vaciar(lista_voltajes)
}
El pseudocódigo es muy elemental pero ese es el esquema general del proyecto que se siguió al momento de diseñar el programa final. Obviamente la implementación es sustancialmente más compleja que el pseudocódigo pero la realidad es que lo subsecuente fue un tema más de ingeniería de implementación que de diseño.
Diseño de la interfaz gráfica de usuario
Entre los diseños comerciales existen múltiples diseños de osciloscopio pero todos respetan una serie de lineamientos más o menos univerales. En los primeros osciloscopios analógicos ya se incluían:
- Pantalla rectangular dividida en 10 ó 12 secciones en horizontal, en 8 ó 10 secciones en vertical.
- Capacidad de modificar la escala en el eje X, en el eje Y.
- La positibilidad de mover los valores graficados tanto en el eje X como Y.
- Manejo de múltiples canales de lectura(generalmente dos)
En los osciloscopios digitales modernos existen una serie de ventajas adicionales que en los osciloscopios más clásicos no, esto es gracias al aprovechamiento de las capacidades de los procesadores más y más modernos con los que se diseñan.
- Detección de frecuencia
- Cálculo automático de voltaje de corriente directa
- Caracterización del voltaje de corriente alterna
- Centrado automático de los picos en señales con oscilación (i.e. centrado de una sinusoidal)
- Dibujo de líneas de referencia
La funcionalidad "fundamental" es aquella que podríamos asociar con un osciloscopio analógico clásico. Las capacidades más complejas que se encuentran en los osciloscopios modernos fueron considerados ganancia adicional.
Ya con los lineamientos básicos establecidos comenzó el desarrollo de la aplicación usando la biblioteca de "swing" en Java, con la cual es muy fácil programar aplicaciones gráficas.
Después de múltiples diseños se determinó que quizá el mejor para una experiencia de usuario sencilla sería el siguiente:
Entrar en detalle con respecto al funcionamiento preciso de la graficación así como los detalles de manejo del puerto serial etc. estaría fuera de enfoque ya que, una vez más, estaríamos trabajando con temas muy específicos de código y no de diseño.
Se tratarán más adelante los detalles con respecto a todas estas minucias.
Usuario avanzado: Diseño código Java
Usuario avanzado
Circuito
Aquí veremos con todo detalle y minucia el diseño del circuito así como el proceso de diseño que llevó al modelo final. Determinaremos el valor de los resistores paso a paso. Demostraremos cada paso con rigor y también agregaremos simulaciones de circuitos para su manipulación y revisión.
Programa ESP-32
Veremos el proceso de optimización del código que cargamos a la ESP-32 para dar introspección del diseño del código así como el uso de funciones de bajo nivel de C para obtener el comportamiento deseado de la tarjeta. El código aquí usado se creó pensando en un protocolo de transmisión serial en el que se envían dos bytes conteniendo 12 bits de resolución del ADC. Para realizar modificaciones a este código se necesitará saber muy bien lo que se está haciendo. Igualmente, al modificar este código es muy probable que se tenga que modificar el código de las aplicaciones de java también.
Java
Diseñaremos primero unos programas basados en consola a partir de los cuales crearemos tres programas con interfaz de usuario. Programaremos un generador de funciones sinusoidales, un traductor del puerto serial y un graficador. Todos ellos funcionarán por medio del protocolo UDP.
El programa llamado "traductor" será un programa por medio del cual interpretaremos los valores enviados en el puerto serial. Estos valores los enviaremos a un puerto UDP. Solo este programa será el que interactuará con el mundo externo a la computadora.
El programa llamado "graficador" será un programa por medio del cual podremos visualizar los valores de voltaje que lleguen por medio de un puerto UDP. No importará si estos valores vienen de un "traductor" o de un "generador".
Los programas llamados "generador" será programas por medio de los cuales podremos simular señales dentro de la propia computadora. Crearemos una señal imaginaria y la enviaremos al puerto UDP de forma que se interpretará igual que cualquier señal recibida en un "traductor".
El código trabajado aquí todo se encuentra en los siguientes repositorios públicos:
Diseño a profundidad (circuito)
En este capítulo trataremos con todo detalle y minucia el diseño del circuito al que está conectada la tarjeta ESP-32. Se espera un conocimiento con buenos fundamentos de electrónica para tener un buen seguimiento de esta parte. No se tocarán temas de programación sino únicamente de electrónica.
El simulador con el que se realizó buena parte del diseño fue Proteus sin embargo, la alternativa en línea de Falstad Circuit será la mejor opción para presentarlo en este texto.
Acondicionamiento de señal
Como ya se trabajó en la parte de diseño, en esta sección estamos intentando darle un acondicionamiento a una señal eléctrica la cual desconocemos su origen. Hay unas pocas cosas que tenemos que tener en consideración al momento de diseñar este dispositivo:
- Tendremos dos terminales de medición, como cualquier oscsiloscopio.
- El acondicionamiento de señal tiene que transformar los voltajes de las terminales de entrada en voltajes que se encuentren en un rango de operación seguro para la ESP-32. (0V a 3.3V con respecto a la tierra de la tarjeta)
- Es indispensable poder caracterizar el voltaje sin la necesidad de predicciones en software, es decir, la información entregada por el circuito de acondicionamiento es la única información de la que podremos generar más información.
- La información sumistrada por medio del circuito de acondicionamiento tiene que ser suficiente para calcular los valores del voltaje entre las terminales de medición haciendo uso de un algoritmo replicable en cualquier intervalo de voltaje entre esas terminales. Con esto quiero decir, no podemos hacer uso de una X fórmula para calcular ese voltaje cuando el voltaje es negativo y de una fórmula Y cuando el voltaje sea positivo (por ejemplo).
Hay una serie de puntos adicionales que ya son más bien buenas prácticas que requisitos:
- De usar componentes no lineales (condensadores, inductores, transistores etc.) es indispensable que se mantenga la seguridad de la tarjeta ESP-32 por encima de los componentes de acondicionamiento.
- De usar componentes no lineales el circuito tiene que operar en un rango seguro en cuanto a su frecuencia. Es decir, no podemos permitir la formación de armónicos en el circuito en el rango de frecuencias que esperamos poder medir.
- El dispositivo idealmente se tiene que poder alimentar con un solo cable o una sola fuente de energía. En nuestro caso, ya que estamos trabajando con un cable USB como alimentación de la tarjeta ESP-32, la recomendación es mantenerse en el rango de 0[V] a 5[V] con respecto a la tierra de la tarjeta. Esto quiere decir hay que tener MUCHÍSIMO cuidado al momento de trabajar con voltajes negativos.
- El dispositivo no puede poner en riezgo la integridad de la tarjeta ESP-32 al momento de encendido o apagado inesperado. Esto quiere decir, todos los estados transitorios tienen que estar contemplados y corregidos. Esto es especialmente importante al momento de considerar diseño con baterías.
Implementación con solo resistores
Podemos partir de la implementación que habíamos propuesto previamente. Habíamos propuesto un sistema partiendo de un divisor de voltaje (los resistores R1 y R2) al cual introducimos una desviación de voltaje por medio de un tercer resistor (resistor N). Determinamos que necesitaremos de dos arreglos de estos tres resistores, un sistema A (R1_A, R2_A, RN_A) y un sistema B (R2_A, R2_B, RN_B).
Para realizar el modelado matemático de estos sistemas de resistores podemos trabajar con los esquemas siguientes; partiendo del diseño original crearemos una descomposición de los voltajes en el nodo RN a partir de el análisis por fuentes independientes. Podemos también realizar simplificaciones por resistores paralelos.
Para calcular el valor de los resistores hicimos uso del método de fuentes de voltaje linealmente independientes. Como por diseño sabemos que el resistor de tierra va a tierra, no es necesario realizar el análisis con este resistor.
A partir de estos esquemitas podemos continuar con el análisis para cada una de las fuentes independientes (el voltaje introducido en R1 y el voltaje introducido en N). El voltaje introducido en R1 será uno que llamaremos "V_ref" ya que es el voltaje de referencia de 3.3 V en la tarjeta ESP-32. El voltaje introducido en N lo llamaremos "V_T" ya que es el voltaje de la terminal de lectura del osciloscopio. "V_1" y "V_2" serán los voltajes que contribuye cada una de estas fuentes independientes.
Para la fuente linealmente independiente en R1 (V_Ref) tenemos:
Para la fuente linealmente independiente en N (V_T) tenemos:
Esquemas realizados en OneNote
Así, que podemos caracterizar el voltaje presente en el nodo RN (tanto para la terminal positiva como para la negativa, en el diseño llamados terminal A y B):
\[ V_{RN} = V_{1} + V_{2} \]
\[ V_{RN} = V_{Ref} \frac{N || R_{1}}{N||R_{2} + R_{1}} + V_{T} \frac{R_{1}||R_{2}}{R_{1}||R_{2} + N} \]
Donde el operador || significa "resistor equivalente paralelo".
Podemos simplificar estas expresiones para convertirlo en una expresión únicamente dependiente de sumas y multiplicaciones:
\[ V_{RN} = V_{Ref} \frac{N R_{2}}{N R_{2} + N R_{1} + R_{1} R_{2}} + V_{T} \frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Hay que recordar que para nuestro sistema, N, R1 y R2 estarán todos determinados, el valor de V_ref es conocido (3.3V) y el valor de V_RN es el valor que leeremos con el ESP-32 por lo que el único valor que tendremos que determinar será el valor de V_T. Para conseguir esto únicamente hay que despejar:
\[ (V_{RN} - V_{Ref} \frac{N R_{2}}{N R_{2} + N R_{1} + R_{1} R_{2}}) \frac{R_{1} R_{2} + N R_{1} + N R_{2}}{R_{1} R_{2}} = V_{T}(V_{RN}) \]
Parece un poco aparatoso pero la realidad es que como todos los valores son constantes, por esto, todo se podrá simplificar a un par de números reales. De hecho, a simple vista se puede ver que ya existen ciertos factores que se pueden simplificar sin demasiados trucos algebráicos.
Ya que tenemos un modelo para describir el voltaje de la terminal T a partir del voltaje del nodo RN, que recordar, ya tenemos una serie de lineamientos que tenemos que cumplir para que nuestro osciloscopio se comporte como queremos; habíamos establecido qué comportamiento queríamos en los puntos límite del voltaje que consideramos operativo "V_Max" y "V_Min" de los cuales también deducíamos un voltaje "V_cero".
Recordando rápidamente:
\[ V_{RN}(V_{Min}) = 0[V] = V_{Min} * K_{Atenuación} + C_{Ajuste} \] \[ V_{RN}(V_{Max}) = 3.3[V] = V_{Max} * K_{Atenuación} + C_{Ajuste} \] \[ V_{RN}(V_{cero}) = 1.65[V] = V_{cero} * K_{Atenuación} + C_{Ajuste} \] \[ V_{Terminal} \in (V_{min}, V_{max})\]
Aquí tenemos una serie de requerimientos por cumplir y, por diseño, tenemos que determinar varias de ellas para poder determinar el sistema. En realidad, no es difícil ver que la parte de
\[ V_{Min/Max} * K_{Atenuación} + C_{Ajuste} \]
la vamos a tener que sustituir por la definición que ahorita obtuvimos para el voltaje en RN en función del voltaje de la terminal T en el resistor N.
Por facilidad de diseño se decidió que el voltaje mínimo fuera el mismo voltaje pero con signo negativo que el voltaje máximo:
\[ V_{Min} = - V_{Max} \]
Esto inmediatamente (como lo descubrimos en el modelado lineal) nos determina que el valor "central" (el que llamams Voltaje "cero" en la fase de diseño) sería exactamente 1.65[V] para nuestra tarjeta ESP-32. Esto quiere decir, cuando nosotros leamos 1.65 en RN significará que el valor real de voltaje aplicado en la terminal T será de 0[V] con respecto a la tierra.
Poniendo el voltaje en el nodo RN en función del voltaje de la terminal T se describiría así:
\[ V_{RN}(V_{T}) = V_{T} * K_{Atenuación} + C_{Ajuste} \] \[ V_{RN}(0[V]) = 1.65[V] \]
En realidad estamos sobre-definiendo nuestro sistema ya que, sabiendo que nuestro sistema es lineal, únicamente necesitaremos determinar los valores del voltaje Máximo y Mínimo en la terminal T para determinar todo lo demás.
Regresando a la simplificación de nuestra caracterización de voltaje de la terminal T en función de el voltaje en RN:
\[ V_{T}(V_{RN}) = (V_{RN} - V_{Ref} \frac{N R_{2}}{N R_{2} + N R_{1} + R_{1} R_{2}}) \frac{R_{1} R_{2} + N R_{1} + N R_{2}}{R_{1} R_{2}} \]
Simplificando obtenemos:
\[ V_{T}(V_{RN}) = V_{RN} \frac{R_{1} R_{2} + N R_{1} + N R_{2}}{R_{1} R_{2}} - V_{Ref} \frac{N}{R_{1}} \]
Si deciéramos obtener el voltaje en RN en función del voltaje de la terminal en vez de el voltaje de la terminal en funcion de el voltaje en RN nos quedaría algo muy parecido al sistema que propusimos de:
\[ V_{RN}(V_{T}) = V_{T} * K_{Atenuación} + C_{Ajuste} \]
Comprobémoslo para convencernos que el sistema es correcto:
\[ V_{RN}(V_{T}) = V_{T} \frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} + V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Resulta bastante convincente que en efecto estamos caracterizando el fenómeno correcto.
Ya que llegamos a este punto es necesario decidir si nuestras restricciones de diseño serán los voltajes mínimos y máximos o si serán los resistores que podemos utilizar para crear nuestra K de atenuación y nuestro C de ajuste.
Como decisión de diseño, personalmente recomiendo considerar los voltajes máximos y mínimos que uno se va a encontrar como la variable de mayor peso. Los resistores se puede comprar por un precio relativamente accesible y así cambiar el diseño por completo, sin embargo, los voltajes que encontraremos en el mundo están más o menos determinados y es bastante fácil construir a partir de esa información.
Por ejemplo, el voltaje de la electricidad en un enchufe común es de 127[V] RMS en México. Esto quiere decir que estaríamos trabajando con un voltaje Pico a Pico de 359.2 [V], en realidad el voltaje que se suministra en el país es bastante más inestable de lo que nos gustaría por lo que valdría la pena agregar un buen margen de seguridad; pensemos que el máximo voltaje que nos podríamos encontrar fuera uno Pico a Pico de 500 [V] solo para tener suficiente margen de error y garantizar la seguridad de nuestro dispositivo.
En este sentido, podríamos crear un sistema de resistores para atenuación de grandes voltajes (como el voltaje suministrado por un enchufe). Al mismo tiempo tendríamos que crear un sistema que nos diera atenuación para voltajes de uso de laboratorio (voltajes entre -20[V] y +20[V], por ejemplo).
Así tendríamos dos pequeños arreglos de resistores con los cuales podríamos medir prácticamente cualquier voltaje a nuestro alcance sin tener que sufrir una atenuación excesiva en voltajes pequeños.
Entonces, determinemos los resistores necesarios:
Personalmente me gusta comenzar con la ecuación que describe el voltaje "cero" ya que por medio de ese podemos poner rápidamente un resistor en función de los otros dos. Esto es gracias a que podemos obviar la influencia del voltaje de la terminal T.
\[ V_{RN}(V_{T}) = V_{T} \frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} + V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Sustituyendo el voltaje en la terminal T por cero (ya que asumimos que en este punto no estamos dando ningún voltaje) esperamos obtener un voltaje en RN de 1.65[V]:
\[ V_{RN}(0) = 1.65 = V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Cambiando por el valor del voltaje de referencia de operación (3.3) obtenemos una manera rápida de poner un resistor en función de los otros.
\[ V_{RN}(0) = V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Pensando en sustituir el resistor R1:
\[ R_{1} = \frac{N R_{2} * \frac{V_{Ref}}{V_{RN}} - N R_{2}}{R_{2} + N} = \frac{N R_{2}(\frac{V_{Ref}}{V_{RN}} - 1)}{R_{2} + N} \]
Tenemos que recordar que en este escenario V_Ref es 3.3[V] y V_RN es 1.65[V] por lo que podemos sustituir de una vez y volver a simplificar:
\[ R_{1} = \frac{N R_{2}(\frac{3.3}{1.65} - 1)}{R_{2} + N} = \frac{N R_{2}(2 - 1)}{R_{2} + N} = \frac{N R_{2}}{R_{2} + N}\]
¡Nada mal!
Hagamos brevemente un análisis del resultado aquí obtenido. Si nos fijamos, el valor de R1 lo acabamos de determinar como el circuito paralelo entre R2 y N. Revisemos rápidamente si esto tiene sentido.
Dado que estamos anticipando que cuando el voltaje sumistrado en T sea de 0[V] esperaríamos que el voltaje en el nodo RN sea exactamente el voltaje que se le está suministrando al Resistor 1 por medio del nodo del VRef. Es decir, el resistor equivalente entre R2 y N debería de ser igual a la resistencia en R1. Parece ser que en efecto, tiene sentido al menos en este escenario.
Ahora podemos sustituir el Resistor 1 en alguna otra de nuestras ecuaciones y deberíamos obtener alguna otra caracterización; pensemos en describir R2 a partir de N en la ecuación usada para el voltaje máximo:
\[ V_{RN}(V_{Max}) = V_{Max} \frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} + V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Sustituimos el valor del Resistor 1 por su equivalente descrito en términos de R2 y N:
\[ R_{1} = \frac{N R_{2}}{R_{2} + N}\]
\[ V_{RN}(V_{Max}) = V_{Max} \frac{\frac{N R_{2}}{R_{2} + N} R_{2}}{\frac{N R_{2}}{R_{2} + N} R_{2} + N \frac{N R_{2}}{R_{2} + N} + N R_{2}} + V_{Ref} \frac{N R_{2}}{\frac{N R_{2}}{R_{2} + N} R_{2} + N \frac{N R_{2}}{R_{2} + N} + N R_{2}} \]
Ahora intentaremos poner R2 en términos de N, V_Rn(V_max), V_ref y V_Max. Haciendo algo de álgebra obtenemos que:
\[ N = \frac{R_{2}(2 V_{RN} - V_{Max} - V_{Ref})}{V_{Ref} - 2 V_{RN}}\]
Es difícil identificar de manera inmediata algo que nos revele que la información que obtuvimos aquí sea la correcta, sin embargo, podemos convencernos ligeramente con el análisis dimensional del resultado obtenido. Tenemos que:
\[ Resistencia = Resistencia * \frac{Voltaje}{Voltaje} \]
Esto nos puede dar al menos cierto nivel de garantía que nuestro análisis no está tan mal.
Podemos hacer unos pocos cambios para intentar dejar los números resultantes de las restas como positivos ya que quizá eso nos ayude a interpretar los valores obtenidos:
\[ N = R_{2} \frac{-(2 V_{RN} - V_{Max} - V_{Ref})}{-(V_{Ref} - 2 V_{RN})} = R_{2} (\frac{V_{Max} + V_{Ref} - 2 V_{RN}}{ 2 V_{RN} - V_{Ref}}) \]
Tampoco es demasiado visible pero parecería que podemos asumir que el valor del factor entre paréntesis será un número positivo siempre que nos encontremos dentro de nuestro intervalo de voltaje. Si este número resultara negativo estaríamos metidos en un tremendo problema ya que no existen resistores con valor negativo.
Analisemos por partes, nominador y denominador:
En el nominador, por diseño V_Max es siempre más grande que V_RN, V_Ref también lo es durante todo nuestro intervalo de voltajes de operación así que, por esta parte vamos bien, esto es sin importar el valor de V_RN (el cual, hay que recordar que depende de V_T). El único valor en el que V_RN y V_Ref pueden ser iguales es cuando estemos aplicando en V_T un voltaje igual a V_Max.
Es importante recordar, V_RN es siempre función del voltaje en la terminal T (V_T) ya que V_T es el valor que hace que oscile el valor en RN, el voltaje de V_Ref es siempre constante.
\[ V_{Max} >> V_{RN} \] \[ V_{Ref} >= V_{RN} \]
Así que en el nominador todos los valores son positivos.
En el denominador tenemos que hacer uso del hecho que estamos trabajando con V_Max en la terminal T. Hay que recordar que este sistema de ecuaciones lo resolvimos simbólicamente pero en realidad:
\[ V_{RN}(V_{Max}) = V_{Ref}\]
Esto lo establecimos por diseño. De lo mismo se sigue:
\[ 2V_{RN} - V_{Ref} = 2V_{Ref} - V_{Ref} \]
Como el valor de voltaje de referencia es positivo (en nuestro caso es de 3.3[V]) igualmente tenemos un valor positivo. En realidad, con este conocimiento ya explícito podemos simplifcar toda la expresión de N en función de R2.
\[ N = R_{2} \frac{V_{Max} + V_{Ref} - 2 V_{RN}}{ 2 V_{RN} - V_{Ref}} = R_{2} \frac{V_{Max} + V_{Ref} - 2V_{Ref}}{2V_{Ref} - V_{Ref}} = R_{2} \frac{V_{Max} - V_{Ref}}{V_{Ref}}\]
¡Lindo!
Ahora ponemos rápidamente R2 en términos de N (que era la intención todo esto):
\[ R_{2} = N \frac{V_{Ref}}{V_{Max} - V_{Ref}}\]
Aquí tenemos que tomar una nueva decisión de diseño ¿Cómo determinaremos el primer resistor N de nuestro sistema?
La decisión que yo considero más prudente es determinar el resistor N a partir del voltaje máximo que se aplicará en la terminal T. Esto es porque, a pesar de que tengamos una idea más o menos buena de cuánto es el voltaje máximo que le vamos a aplicar a nuestro osciloscopio, es indispensable agregar un pámetro de seguridad adicional. En especial, con las mediciones en el mundo real, es muy muy común que el componente de tierra no sea el mismo para nuestra computadora (y por extensión en nuestra ESP-32) y la tierra del fenómeno que estamos intentando medir. Pensemos en que nuestra computadora funcionara por medio de una batería (como es el caso de las computadora portátiles), no tenemos ninguna garantía que la tierra de la batería de la pila sea la misma que la tierra de un generador se señales conectado a la corriente de un enchufe. En realidad, dado que están desacopladas ambas fuentes de voltaje, existe garantía que de que la tierra no sea la misma. Lo más probable es que el voltaje de diferencia entre tierras de la batería y la red eléctrica no sea de demasiados voltios, pero sí puede ser de unos cinco o diez voltios en condiciones no tan anómalas (apagones, generadores de emergencia, baterías de coche, etc.) Es por eso que recomiendo darle un parámetro de seguridad a nuestro diseño de al menos 20%. Tendremos una sobre-atenuación, sin embargo, nuestro modelo lo contemplará de la manera correcta.
Recordemos que es indisepensable garantizar la seguridad de la ESP-32, eso quiere decir, el voltaje presente en RN no puede ser mayor a 3.3[V] ni menor a 0[V].
Correremos riesgo de exceder los 3.3[V] cuando V_T sea máximo y de irnos por debajo de los 0[V] cuando V_T sea mínimo. Podemos plantear un circuito de ejemplo para determinar entonces nuestro primer resistor N.
Ya que R1 y R2 serán mucho más chiquitos que el resistor N, podemos considerarlos de buenas a primeras resistores de 330 ohmios ya que además van a ser resistores de valores más o menos semejantes y únicamente preocuparnos por la disipación en el resistor N. Esto nos dará un valor propuesta para el resistor N, el cual podemos multiplicar por algún factor de seguridad y, así, posteriormente cambiar el valor de R1 y R2 siguiendo la formulación que realizamos previamente donde R1 y R2 dependen de N. Ya que el voltaje de V_T es mucho mucho más grande que V_Ref podemos incluso considerar que V_Ref es una fuente apagada.
Suponiendo un resistor inicial de:
Y una simplificación haciendo análisis por fuentes independientes:
Entonces, el voltaje en RN queda caracterizado en este primer modelo propuesto: \[ V_{RN} = V_{T} \frac{R_{1} || R_{2}}{R_{N} + R1||R2} \]
Ahora necesitamos nuestro valor propuesta de V_T. Este valor a proponer, como ya dijimos, tiene que ser el valor de voltaje máximo que esperemos encontrar en nuestros escenarios de lectura. Por el bien del ejemplo y únicamente para ser congruente con los valores propuestos en la parte del armado, usaremos un valor de 50[V]. Esto quiere decir, nuesto osciloscopio será capaz de medir valores de voltaje de +50[V] y -50[V] con respecto a su tierra de manera segura y bastante exacta.
\[ 3.3 = 50 \frac{165}{R_{N} + 165} \]
De donde obtenemos el valor del resistor N:
\[ N = \frac{50 * 165}{3.3} - 165 = 2335\]
Considerando ahora en valores de resistores comerciales, podemos ver que un resitor de 3300 ohmios cumple con una tolerancia adicional de aproximadamente 30%. Así que lo definimos:
\[ N = 3300\]
Ya determinado nuestro resistor N proseguimos a determinar los resistores R1 y R2 siguiendo las fórmulas que demostramos antes:
\[ R_{2} = N \frac{V_{Ref}}{V_{Max} - V_{Ref}} = 3300 \frac{3.3}{50 - 3.3} = 233.19\]
\[ R_{1} = \frac{N R_{2}}{R_{2} + N} = \frac{3300 * 233.19}{3300 + 233.19} = 217.79\]
En efecto, obtuvimos resultados más o menos como lo esperábamos.
Ahora probemos el dispositivo que acabamos de diseñar únicamente para revisar su validés, una vez más haciendo uso de un simulador.
¡Lo logramos!
¡En teoría y en simulación el modelo funciona!
Es obvio que sería muy difícil conseguir resistores de 218 ohmios y de 233 ohmios. Entonces tenemos que una vez más considerar que el necesitaremos resistores comerciales.
218 lo podemos conseguir diréctamente con solo 1% de desviación si usamos uno de 220 ohmios. El de 233 lo podemos conseguir usando uno de 180 ohms y sumándole uno de 51 ohms esto nos dejaría con una desviación también menor a 1%.
Así, después de toda esta trayectoria completamos el arreglo de resistores que queríamos armar:
- N = 3300 Ohmios
- R1 = 220 Ohmios
- R2 = 180 Ohmios + 51 Ohmios
Hay que recordar que tenemos que armarlo tanto para la terminal A (positiva) como la terminal B (negativa) por lo que necesitaremos dos de cada uno.
El último paso a tomar en esta parte es deducir una fórmula fácil de calcular rápidamente para calcular el voltaje en T a partir del voltaje en RN. Si recordamos la fase anterior de diseño, cuando usamos fuentes de voltaje independientes, obtuvimos:
\[ V_{RN}(V_{T}) = V_{T} \frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} + V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}} \]
Con una fácil manipulación podemos obtener el valor del voltaje en la terminal T con respecto al voltaje de RN.
\[ V_{T}(RN) = \frac{V_{RN} - V_{Ref} \frac{N R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}}}{\frac{R_{1} R_{2}}{R_{1} R_{2} + N R_{1} + N R_{2}}}\]
Para obtener un poco más de rendimiento en la computadora podemos simplificar las divisiones:
\[ V_{T}(V_{RN}) = V_{RN}(\frac{R_{1} R_{2} + N R_{1} + N R_{2}}{R_{1} R_{2}}) - V_{Ref} (\frac{N R_{2}}{R_{1} R_{2}}) = \frac{V_{RN}(R_{1} R_{2} + N R_{1} + N R_{2}) - V_{Ref}(N R_{2})}{R_{1} R_{2}} \]
Resumido:
\[ V_{T}(V_{RN}) = \frac{V_{RN}(R_{1} R_{2} + N R_{1} + N R_{2}) - V_{Ref}(N R_{2})}{R_{1} R_{2}} \]
Cancelación del término independiente
Así, obtuvimos una caracterización del voltaje en las terminales T. Sin embargo, tenemos que recordar que no nos interesa conocer el voltaje de las terminales de manera independiente, nos interesa la diferencia de los voltajes.
Pensemos que las terminales de en las que estamos midiendo el voltaje se llamaran "A" en la terminal positiva y "B" en la negativa. (En este caso, la B se comportará más bien como el punto de referencia).
\[ V_{T_{A}} - V_{T_{B}} = (K_{V_{RN_{A}}} + C_{Ajuste A}) - (K_{V_{RNB}} + C_{Ajuste B})\]
Sin embargo, podemos analisar brevemente cuál es el valor de C de ajuste y encontrar que el valor es el mismo para ambos circuitos.
\[ V_{T}(V_{RN}) = \frac{V_{RN}(R_{1} R_{2} + N R_{1} + N R_{2})}{R_{1} R_{2}} - \frac{V_{Ref}(N R_{2})}{R_{1} R_{2}} \]
Podemos ver que únicamente el sumando del lado izquierdo es dependiente del valor aplicado en el nodo RN. El sumado del lado derecho es dependiente de los resistores y el valor de voltaje de referencia, no de la variable independiente. Como la caracterización del voltaje en el nodo RN es el mismo tanto para el nodo RNA como para el nodo RNB, podemos fácilmente simplificar.
\[ V_{T}(V_{RN_{A}}) - V_T{T}(V_{RN_{B}}) = \frac{V_{RN_{A}}(R_{1} R_{2} + N R_{1} + N R_{2})}{R_{1} R_{2}} - \frac{V_{Ref}(N R_{2})}{R_{1} R_{2}} - (\frac{V_{RN_{B}}(R_{1} R_{2} + N R_{1} + N R_{2})}{R_{1} R_{2}} - \frac{V_{Ref}(N R_{2})}{R_{1} R_{2}}) \]
Simplificado:
\[ \Delta(V_{TAB}) = V_{T}(V_{RN_{A}}) - V_{T}(V_{RN_{B}}) = \frac{V_{RN_{A}}(R_{1} R_{2} + N R_{1} + N R_{2})}{R_{1} R_{2}} - \frac{V_{RN_{B}}(R_{1} R_{2} + N R_{1} + N R_{2})}{R_{1} R_{2}}\]
\[ \Delta(V_{TAB}) = (V_{RN_{A}} - V_{RN_{B}})(\frac{R_{1} R_{2} + N R_{1} + N R_{2}}{R_{1} R_{2}})\]
Esta será la función que se utilizará para determinar el valor de la diferencia de voltajes en las fases siguientes del diseño.
Recomendaciones a futuro
Si ha llegado a este punto del manual le deseo toda la suerte y le transmito todo el entusiasmo del mundo. Como puede ver, el diseño está lleno de recovecos y errores que pueden surgir durante el proceso. Le agradezco muchísimo haberme seguido en este proceso de diseño.
No hay mayor muestra de agradecimiento que le pueda dar que dejarle consejos con respecto al diseños posteriores que quiera hacer como modificaciones a este proyecto así como proyectos personales de otra índole.
- No lo haga.
- De verdad, no lo haga.
- Lo digo con toda la sinceridad del mundo, utilice diseños de otras personas en las que confíe, diseñar las cosas desde cero resulta más un proceso de crecimiento personal que algo de verdadera utilidad.
Si en verdad quiere hacerlo o lo necesita:
- El proceso de diseño es arduo y lleno de errores, para llegar a este último diseño se tuvo que pasar por al menos otras dies propuestas de diseño, todas revisadas acompañado de gente con más experiencia que yo.
- Siempre consulte cada paso que dé con alguien más, mientras más fácil de explicar más fácil será de diseñar también.
- Si puede, pregúntele a profesores o gente con más experiencia que usted sobre la tecnología que conocen y la que han usado. Eso puede orientar mucho la fase de diseño además de tener cerca una persona que podrá entender los diseños propuestos sin necesidad de demasiado contexto.
Con respecto al osciloscopio
Existen muchos sitios en donde se podrían agregar soluciones más complejas según el uso que usted usuario le quiera dar:
- Convertirlo en un dispositivo autónomo (no dependiente de un cable para alimentación)
- Realisar análisis únicamente de voltajes en corriente alterna para análisis de señales
- Obviar el análisis de señales para convertir este dispositivo en un multímetro
- Agregar capacidad de medición de corriente
Hay muchas cosas que no se contemplaron en el diseño de este osciloscopio, sin embargo, queda en usted lector. Este proyecto no es bajo ninguna definición perfecto.
El diseño de electrónica analógica es difícil, intente usar los componentes que mejor conoce:
- Resistores
- Condensadores
- Amplificadores operacionales sencillos
Intente reducir el problema que quiere resolver a estos componentes antes de comenzar a usar piezas más complejas.
El único verdadero requisito en esta parte de diseño es haga "embonar" un voltaje de -Vmax a Vmax en el intervalo de 0[V] a 3.3[V]. Mi recomendación es que lo logre con una función tan sencilla como lo hicimos en esta solución:
\[ V_{RN}(V_{T}) = V_{T} * K_{Atenuación} + C_{Ajuste} \]
Si decidiera realizar un diseño en el cual ya se contemple desde el acondicionamiento de señal la resta de los voltajes entre las terminales de lectura A positiva y B negativa tendrá que tener un poco de cuidado nada más con la parte del proyecto subsecuente La programación de la tarjeta ESP-32 ya está programada contemplando la resta de voltajes entrantes por dos diferentes terminales, por lo que tendrá que realizar modificaciones ahí también.
Le deseo la mejor de las suertes; este es sin lugar a duda el lugar donde más oportunidad existe para el diseño, sin embargo, también considero que es el lugar donde más dificultades encuentra uno.
Diseño a profundidad (ESP-32)
Para el correcto seguimiento de esta sección del libro se espera un conocimiento moderadamente avanzado del lenguaje de programación C así como un conocimiento básico de señales.
Dada la naturaleza de nuestro proyecto, la tarjeta ESP-32 únicamente funciona como una interfaz entre el mundo real y el mundo digital en el cual queremos representar las gráficas del voltaje de nuestro osciloscopio.
En el capítulo de diseño habíamos mencionado un pseudocódigo a partir del cual nos vamos a basar para programar, ahora sí, con código optimizado la tarjeta ESP-32 para obtener la mayor velocidad de muestreo posible así como el comportamiento más homogéneo a lo largo del tiempo que podamos conseguir.
Comencemos con el pseudocódigo original, convirtámoslo en código de Arduino y comencemos a optimizar:
int lectura_ADC_A
int lectura_ADC_B
int pin_ADC_A
int pin_ADC_B
int resta_ADC_AB
inicializa(){
//De este valor dependerá la velocidad de operación de la ESP32
configurar_reloj_ESP32
//De este valor dependerá nuestra velocidad de envío
configurar_tasa_baudios_monitor_serial(máximo_valor_estable)
//De este valor dependerá nuestra calidad de lectura
configurar_resolución_ADC(máximo_valor_estable)
}
ciclo(){
//Leemos los valores de las terminal positiva (A) y la terminal negativa (B)
lectura_ADC_A = leer(pin_ADC_A)
lectura_ADC_B = leer(pin_ADC_B)
//Al valor de la terminal positiva restarle el valor de la terminal negativa
resta_ADC_AB = lectura_ADC_A - lectura_ADC_B
//Mandamos el valor a la computadora
mandar_serial(resta_ADC_AB)
}
Convirtiéndolo en código arduino podemos hacer una traducción casi palabra por palabra:
//El número de pin en que esperaremos leer los valores de voltaje
#define pin_ADC_A 4
#define pin_ADC_B 6
//Variables para guardar la lectura 0->4095
int lectura_ADC_A = 0;
int lectura_ADC_B = 0;
int resta_ADC_A_B = 0;
void setup() {
//Configuramos los ADC a su máxima resolución
//Por defecto es de 12 bits por lo que podemos comentar casi siempre
//analogReadResolution(12);
//Configuramos la velocidad de reloj a la máxima posible
//Generalmente esta velocidad está predeterminada a 240, comentamos
//setCpuFrequencyMhz(240);
//Comenzamos el monitor serial a la máxima velocidad estable
Serial.begin(921600);
}
void loop() {
//Leemos el valor de voltaje presente en los pines A y B
lectura_ADC_A = analogRead(pin_ADC_A);
lectura_ADC_B = analogRead(pin_ADC_B);
//Lectura A - Lectura B
resta_ADC_A_B = lectura_ADC_A - lectura_ADC_B;
Serial.println(resta_ADC_A_B);
}
Ya que está explícito el código, limpiemos un poco. Confie en mi, esto se va a saturar pronto:
#define pin_ADC_A 4
#define pin_ADC_B 6
int lectura_ADC_A = 0;
int lectura_ADC_B = 0;
int resta_ADC_A_B = 0;
void setup() {
Serial.begin(921600);
}
void loop() {
lectura_ADC_A = analogRead(pin_ADC_A);
lectura_ADC_B = analogRead(pin_ADC_B);
resta_ADC_A_B = lectura_ADC_A - lectura_ADC_B;
Serial.println(resta_ADC_A_B);
}
Hay que utilizar la biblioteca oficial de Espressif para poder utilizar el IDE de Arduino y subir el código. Recordar que en la sección de Manual de Usuario "Cómo programarlo" se trató este tema.
Ya preparado esto y subiendo el código a la tarjeta, si hacemos pruebas con nuestro osciloscopio armado (tal y como lo armamos en la fase de manual de usuario) podemos abrir la ventana del monitor serial y visualizar los valores desde el propio IDE de Arduino.
Aquí es donde surge la importancia de las herramientas que habíamos propuesto en la fase de diseño:
Windows y Linux HTerm
Para Mac CoolTerm
Si usamos el IDE de Arduino obtendremos más o menos el siguiente comportamiento:
Pinta bien, si además cambiamos, en vez del monitor serial, usamos el llamado "Serial Plotter" podemos visualizar rápidamente los valores obtenidos desde el EPS-32 en una gráfica.
Ahora, le pediré que confíe en mi cuando le digo, el código está mal y es muy lento. Las pruebas realizadas fueron extensas pero escapan de la fase de diseño y son más bien cuestión de la fase de pruebas.
Para no entrar en demasiado detalle, hay una serie de problemas:
- El código es muy lento por la conversión que realiza la ESP-32 del valor leído del ADC.
- Se lee el valor del ADC en binario
- Se convierte el número a decimal
- Se imprime el número como caracters ASCII en el monitor serial
- Esto supone una conversión adicional de número decimal a número de caracter ASCII lo que implica enviar dos bytes de datos ¡PARA CADA CIFRA DE NUESTRO NÚMERO!
- Ese último punto revela que nuestro código además de ser lento es inconsistente, esto porque los números leídos de una sola cifra (0->9) se imprimirán en un solo caracter lo que significa dos bytes; los números de dos cifras (-9 -> 99) se imprimirán como dos caracteres, lo que significan cuatro bytes. ¡Ni siquiera podemos obtener un comportamiento consistente entre números positivos y negativos. Comparando con el número -4096 (pensando que el voltaje en B es de 4095 y el voltaje en A es de 0) tendríamos que enviar cinco caracteres de la ESP-32 hacia la computadora; en comparación con los valores de un solo caracter (0->9) estaríamos enviando cinco veces más información haciendo de este protocolo terriblemente ineficiente.
Nuevo protocolo de cifrado
Para arreglar este problema, es necesario proponer un nuevo protocolo de cifrado de nuestros números enteros (-4096 -> 4095) de manera que quepan todos en un tamaño constante de bytes para poder reinterpretarlos en la computadora (en el programa de Java).
Hay múltiples posibilidades para lograr esto, sin embargo, aprovechando que el protocolo serial siempre funciona con el envío de bytes, podemos inmediatamente pensar que dos bytes por número es un tamaño suficiente e incluso sobrante.
Pensemos en el valor binario de 4095 (1111_1111_1111) que es un número de doce bits. Ya que estamos intentando meterlo en dos bytes, es muy claro que en efecto podemos enviar este valor:
16 bits 0000_0000 0000_0000
4095 0000_1111 1111_1111
Y tendremos una holgura de 4 bits para enviar información adicional que pueda resultar útil.
Entre la información importante que podríamos querer enviar se encuentra si nuestro número leído es positivo o negativo. Después de todo, estamos trabajando con los valores de dos ADC (terminal positiva y terminal negativa) y estamos realizando una resta de valores de 12 bits. (0 -> 4095) para el positivo (A) y (-0->-4095) para el negativo (B). Esto quiere decir que nuestro rango de posibles valores es en realidad -4096 a 4095, dándonos una extraña resolución de 13 bits a pesar de que nuestro ADC sea de 12 bits.
Esto quiere decir que tendríamos que agregar un bit para enviar el signo:
16 bits 0000_0000 0000_0000
-4096 000s_1111 1111_1111
El siguiente paso se define únicamente por practicidad de la lectura desde el programa de Java. Cuando empezamos a leer el flujo de valores, únicamente empezaremos a leer paquetes de bytes, no sabremos qué número es el inicio y cuál es el fin del número que se está enviando. Podríamos enviar un tercer byte lleno de unos a partir del cual podríamos determinar el fin del número enviado anterior, pero queda claro que aún tenemos tres bits con los cuales podemos enviar esa información.
Podemos definir en nuestro protocolo que si un número inicia con uno será el inicio de un paquete de dos bytes y si inicia con cero será el final del paquete de dos bytes. Por ejemplo, si observáramos lo siguiente:
Sin embargo, dado que nuestro byte menos significativo puede estar lleno de unos, habrá que definir que los dos primeros bits siempre estén vacíos y nosotros los podamos manipular a voluntad para enviar los bits que signifiquen "inicio de número" "fin de número" así como el bit de "número negativo".
0010_1110 //fin de número (lo descartamos)
1001_1011 //inicio de número
0011_1100 //fin de número
1001_1001 //inicio de número
...
Entonces, si el bit con la letra "i" significa "inicio", "f" significa "fin" y "s" es de "signo" podríamos así fácilmente interpretar los números a partir del paquete conformado por los bits señalados con ##_####.
is##_####
fs##_####
is##_####
fs##_####
...
Si definimos que el bit de inicio "i" es 1, el bit de fin "f" es 0, el bit de signo negativo "s" es 1 y el bit de signo positivo "s" es 0, obtendríamos una representación de este estilo:
3094 -> en binario 1100_0001_0110
Visto en paquetes de 6 bits
3094 -> 110000_010110
Lo partimos a la mitad y ponemos los bytes más significativos en el byte de inicio.
Creamos el primer byte a partir del primer paquete de 6 bits
(inicio y positivo) 10_110000
enviamos el primer byte
Creamos el segundo byte a partir del segundo paquete de 6 bits
(final y positivo ) 00_010110
enviamos el segundo byte
Por ejemplo, si las lecturas en el programa de Java fueran:
0001_0110
1011_0000
0001_0110
Podemos descartar el primer byte ya que es un final y no sabemos con qué número inició.
Reordenando el segundo número para darle facilidad visual:
1011_0000 0001_0110
10_110000 00_010110
El primer bit de ambos bytes lo podemos descartar ya que sabemos qué orden tiene el número
0_11000 0_010110
Si el número de inicio es 1 el número será negativo. Si es 0 será positivo.
+ 11000_010110
Convertimos a decimal:
+ (3094) = 3094
¡Funciona!
Ya con esta información, en el programa de Java podremos interpretar cada número con únicamente dos bytes por número. ¡Mucho mejor que el protocolo inicial de cifrado!
Es verdad que tendremos una redundancia con el signo, como no tenemos un uso especial para ese bit y únicamente simplificará el código del cifrado no debemos preocuparnos demasiado por él.
Entonces, ¿cómo quedaría el cifrado en código?
Esta parte ya no es de diseño sino de ingeniería de código. Dada la naturaleza de nuestro algoritmo (en el que vamos a jugar mucho con bits a nivel bajo) es posible que sea un poco difícil de entender qué está pasando, sin embargo, ya con el conocimiento de qué queremos hacer podemos ir paso a paso.
Recordemos que hay que codificar un número binario de dos bytes a un número en nuestra codificación.
Vamos a tener que hacer uso de una serie de mañas de C ya que no tenemos la posibilidad de agregar código ensamblador con el cual hacer estas manipulaciones de bytes.
Comencemos con un ejemplo, el ESP-32 lee el valor en A de 223 y el valor en B de 3359:
Tenemos que conocer el valor de la resta (el cual será negativa en este caso)
223 - 3359 = -3136
Para aquellos con buen ojo se habrán dado cuenta de un detalle que no hemos tocado. El número negativo se representará como un número en su complemento a dos. Yo sé que puede parecer un problema pero no lo es. Es por esto que es tan importante mandar el signo del número desde la ESP-32. Vamos a tratar en el código de Java este tema en concreto.
La representación del número -3136 en binario (aunque parezca raro, justo por el complemento a dos) es la siguiente:
1111_0011 1100_0000
El número que nos interesa es el de los primeros 12 bits y sabemos que el número es negativo y los primeros cuatro unos aparecen únicamente por la representación en 16 bits del número. Sabemos que el número será negativo si y solo si los primeros cuatro bits son todos 1. Esto se debe al llamado "desbordamiento" al momento de hacer restas que vayan por debajo del cero. Lo que vamos a hacer va a ser aprovechar este desbordamiento para encontrar el signo del número que vamos a enviar de la ESP-32 a la computadora.
Entonces, si vemos el número "desbordado" en 16 bits dado que nuestro número es uno de 12 bits, sabremos que el número en cuestión es un número negativo.
if (x > 4096){
//El número es negativo
//Por la cadena de 1111
//1111_xxxx_xxxx_xxxx
}
Sin embargo, ni siquiera es necesario hacer la condicional, podemos diréctamente poner ese 1 como el valor que nos interesa en el número de 16 bits ya cifrado.
Entonces:
- Usaremos dos bytes, vacíos, a esos dos bytes les pondremos los doce bits marcados con xxxx_xxxx_xxxx.
- Agregaremos el signo, que como 1 es nuestro negativo, podemos escribirlo diréctamente de los 1111 desde el desbordamiento.
- Limpiamos la cola de 1111 del primer byte y agregamos los 6 bits más significativos al primer byte del paquete y los menos significativos al segundo byte del paquete
- Etiquetamos con un 1 el primer bit para el primer byte
- Etiquetamos con un 0 el primer bit del segundo byte (cosa que podemos no hacer porque el número inicia siendo puros ceros).
- Imprimimos como bytes (NO COMO TEXTO) de la ESP-32 a la computadora
Paso a pasito, para no perdernos en la manipulación de bits:
//El número de pin en que esperaremos leer los valores de voltaje
#define pin_ADC_A 4
#define pin_ADC_B 15
//Usaremos short que es una estructura de 16 bits
short lectura_ADC_A = 0b0;
short lectura_ADC_B = 0b0;
short resta_ADC = 0b0;
//Estas serán las variables que enviaremos por medio del serial
byte num_codificado_primero = 0b0;
byte num_codificado_segundo = 0b0;
void setup() {
Serial.begin(921600);
}
void loop() {
//Inicializamos los valores de nuestros bytes
//Podemos poner el etiquetado del primero byte de una vez
num_codificado_primero = 0b10000000;
num_codificado_segundo = 0b00000000;
lectura_ADC_A = analogRead(pin_ADC_A);
lectura_ADC_B = analogRead(pin_ADC_B);
resta_ADC = lectura_ADC_A - lectura_ADC_B;
//Recordemos que resta_ADC es un número de 16 bits
//Los números codificados son de un solo byte
//Por esto tenemos que recorrerlo un byte
//Para tener acceso a la cola de 1111
//Aquí estamos sumando el segundo 1, 0100
//Por eso aplicamos una máscara lógica &&
num_codificado_primero += ((resta_ADC >> 8) && 0b01000000);
num_codificado_segundo += ((resta_ADC >> 8) && 0b01000000);
//Aplicando otra máscara lógica sumamos los 6 bits
//Tenemos que recorrer 6 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxxx_xxxx
// isxx_xxxx -> is_xxxx_xx
num_codificado_primero += ((resta_ADC >> 6) && 0b00111111);
//Tenemos que recorrer 0 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxyy_yyyy
// isxx_xxxx -> isxx_xxxx
num_codificado_segundo += (resta_ADC && 0b00111111);
Serial.print("Resta:");
Serial.println(resta_ADC);
Serial.print("1ero:");
Serial.println(num_codificado_primero);
Serial.print("2ndo");
Serial.println(num_codificado_segundo);
}
Solo para revisar rápidamente en el monitor serial si se está enviando correctamente nuestro número, podemos usar Serial.print() para revisar los valores enviados. En todos las implementaciones ya funcionales usaremos Serial.write() además ya no imprimiremos las banderas de "Resta, 1ero, 2nd".
Entonces, ¿qué obtenemos?
Si conectamos ambas terminales (positiva y negativa) al mismo voltaje de tierra, vamos a obtener un resultado más o menos como el mostrado. Interpretémoslo:
El primer valor que estamos imprimiendo es la resta de los valores del ADC_A - ADC_B, recordemos que estos son números entre 0 -> 4095 por lo que valores como -31 son en realidad bastante cercanos al cero. Esto es congruente con la lectura que estamos haciendo de la tierra, esperaríamos un valor leído de 0, sin embargo, por ruido generado por el efecto de antena de los cables así como errores en los resistores con los que estamos trabajando también traerá un poco de desviación. (Además recordemos que estamos trabajando con un circuito diseñado con un 1% de error).
Los segundos valores son "1ero" que es el valor del primer byte, en el cual vamos 255 para todos los mostrados en la captura. El tercer valor impreso es "2ndo" que es el segundo byte de nuestro paquete, este oscila entre 97 y 91 en la captura, pero para ser congruentes con la lectura de la "resta" vamos a usar la primera lectura:
Resta: -38
1ero : 255
2ndo : 90
Esta información es suficiente como para saber si nuestra codificación fue correcta, veamos. Tendremos que hacer la traducción de -38 a dos bytes 255_90 o esos dos byte a -38. Considero que es más fácil hacerlo de los dos bytes a -38, en especial por el hecho de que el número es negativo, y eso es algo que tenemos que explicar rápidamente (como decíamos más arriba con el complemento a 2).
255 = 1111_1111
90 = 0101_1010
El orden de los dos bytes es correcto (como vemos por el 1 y 0 en los primeros bits) También sabemos que el número que estamos leyendo es negativo (como vemos en el segundo bit 1 de ambos bytes)
1ero = 11_1111
2ndo = 01_1010
Juntos = 11_1111_01_1010 -> 1111_1101_1010
Este número es 4058 en binario. Sin embargo, hay que recordar que el número es negativo, esto significa que en realidad estamos trabjando con el complemento a 2 del número en una representación de 12 bits.
Si sacamos el complemento a 2 de este número obtendremos el valor real del número pero tendremos que considerarlo negativo:
1111_1101_1010 -> 0000_0010_0101 + 1 -> 0000_0010_0110 = 38
Con su signo menos: -38
Así que, en efecto, hemos enviado correctamente el mensaje. Ya que sabemos que nuestro código es funcional, podemos cambiar el Serial.print() por Serial.write() y obviar los comentarios de "Resta, 1ero, 2ndo":
Código final
//El número de pin en que esperaremos leer los valores de voltaje
#define pin_ADC_A 4
#define pin_ADC_B 15
//Usaremos short que es una estructura de 16 bits
short lectura_ADC_A = 0b0;
short lectura_ADC_B = 0b0;
short resta_ADC = 0b0;
//Estas serán las variables que enviaremos por medio del serial
byte num_codificado_primero = 0b0;
byte num_codificado_segundo = 0b0;
void setup() {
Serial.begin(921600);
}
void loop() {
//Inicializamos los valores de nuestros bytes
//Podemos poner el etiquetado del primero byte de una vez
num_codificado_primero = 0b10000000;
num_codificado_segundo = 0b00000000;
lectura_ADC_A = analogRead(pin_ADC_A);
lectura_ADC_B = analogRead(pin_ADC_B);
resta_ADC = lectura_ADC_A - lectura_ADC_B;
//Recordemos que resta_ADC es un número de 16 bits
//Nosotros trabajamos con dos bytes separados
//Por esto tenemos que recorrer el entero 8 bits
//Así tenemos acceso a la cola de desbordamiento 1111 (ó 0000)
//Aquí estamos sumamos el segundo bit, donde va el signo
//Por eso aplicamos una máscara lógica & 0b0100
num_codificado_primero += ((resta_ADC >> 8) & 0b01000000);
num_codificado_segundo += ((resta_ADC >> 8) & 0b01000000);
//Aplicando otra máscara lógica sumamos los 6 primeros bits
//Tenemos que recorrer 6 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxxx_xxxx
// isxx_xxxx -> is_xxxx_xx
num_codificado_primero += ((resta_ADC >> 6) & 0b00111111);
//Tenemos que recorrer 0 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxyy_yyyy
// isxx_xxxx -> isxx_xxxx
num_codificado_segundo += (resta_ADC & 0b00111111);
/*
Serial.print("Resta:");
Serial.println(resta_ADC);
Serial.print("1ero:");
Serial.println(num_codificado_primero);
Serial.print("2ndo:");
Serial.println(num_codificado_segundo);
*/
Serial.write(num_codificado_primero);
Serial.write(num_codificado_segundo);
Si intentamos leer los valores mostrados con el código usando Serial.write() no podremos ver ningún valor con sentido en la terminal de Arduino, sin embargo, en CoolTerm o en HTerm sí podremos ver los valores si usamos el modo de visualización en hexadecimal o en binario.
Veremos algo del siguiente tipo:
Aquí, tal como en el análisis que hicimos previamente, podemos ver que hay muchos bytes del tipo "FF" o sea 255 en decimal, que son casi el mismo byte que interpretamos previamente. Esta aplicación está hecha para visualizar el valor de los bytes y no interpretarlos en el cifrado que nosotros diseñamos. Es por eso que el paso subsecuente es interpretar estos valores a una alta velocidad y graficarlos.
Para esto, exploraremos las posibilidades en la siguiente sección de esta capítulo.
Diseños subsecuentes
Si usted lector se siente suficientemente aventurado como para realizarle cambios a este protocolo de comunicación, si por casualidad contara con una tarjeta con una resolución más alta en su ADC y quisiera aprovecharla, aquí dejaré una seried e recomendaciones las cuales espero le sirvan de algo.
A manera de recapitulación, comenzamos con el código más sencillo posible (el que planteamos en el capítulo de diseño) y lo usamos como plantilla para generar este código nuevo. Es una buena idea comenzar con algo ineficiente y malo como plantilla para optimizar y optimizar hasta llegar a un resultado que uno considere suficientemente bueno.
Dado que estamos trabajando con el análisis de señales le recomiendo enormemente que no le tema a la manipulación de los valores binarios en los bytes. C no es el lenguaje más amable al momento de permitirnos estas manipulaciones pero es suficientemente bueno y claro si uno mantiene notas y comentarios como los fuimos haciendo durante nuestro desarrollo.
En caso de tener una tarjeta de mayor resolución de ADC (pensemos que fuera de 16-bits) lo más probable es que tenga que agregar un tercer byte como bandera de inicio/fin de paquetes de datos. La realidad es que es de suma importancia escribir un protocolo de comunicación claro al momento de leerlo. Como se dio cuenta, al momento de leerlo hay ciertos datos que inmediatamente se pueden descartar porque su significado se puede interpretar solo con verlo sin requerir de manipulación posterior.
Evite el uso de condicionales y codificaciones de terceros, después de todo, nadie conoce mejor la implementación del dispositivo que el propio diseñador y el diseñador tiene que estar dispuesto a optimizar su trabajo hasta las últimas consecuencias, no importa si tiene que descartar la tecnología más usada para encontrar algo que se acople mejor a sus necesidades.
Diseño a profundidad (aplicación Java)
En este capítulo diseñaremos un programa sin omitir un solo paso con el cual podremos visualizar los valores del protocolo de cifrado implementado el capítulo pasado.
Se necesita un nivel intermedio de programación para seguir esta guía. No se requiere conocimiento de diseño.
Lector de voltaje
Primero haremos una pequeñísima aplicación, la cual calibraremos usando un generador de señal. Esta aplicación la llamaremos el "mini-multímetro" ya que lo único que hará será imprimir el voltaje leído en consola.
Sería prácticamente imposible plasmar en este manual todas las mañas y trucos de programación que se tuvieron que usar al momento de programar la aplicación final, sin embargo, podemos hacer un intento en programar una versión más chiquita de esta aplicación para intentar transmitir el proceso de diseño.
Como ya dijimos, comenzaremos creando una aplicación chiquta, el mini-multímetro.
mini-multímetro
Toda la programación la realizaré en el IDE IntelliJ Idea Community (por preferencia personal, no por otra razón).
Este ejemplo ni siquiera va a depender de una interfaz gráfica, será todo por medio de consola. Introduciremos más adelante el desarrollo con interfaces gráficas.
- Creamos un nuevo proyecto en Java.
- Descargamos la biblioteca de JSerialComm ya que la necesitamos para el manejo de los puertos seriales.
- Importamos la biblioteca a nuestro proyecto.
- Nuestro primer objetivo será leer los valores de los bytes enviados por el puerto serial, ya después nos preocuparemos por la interpretación.
El primer código que podemos ejecutar (solo para demostrar que todos esté funcionando correctamente) es el siguiente:
package com.marroja;
import com.fazecast.jSerialComm.*;
public class Main {
public static void main(String[] args) {
SerialPort[] puertos = SerialPort.getCommPorts();
for(SerialPort p: puertos){
System.out.println(p.getSystemPortName());
}
}
}
El ejecutar este código deberíamos obtener una salida semejante a esta:
---En Mac y Linux---
cu.wlan-debug
tty.wlan-debug
cu.ESP32_LED_Control
tty.ESP32_LED_Control
cu.Bluetooth-Incoming-Port
tty.Bluetooth-Incoming-Port
cu.usbserial-0001
tty.usbserial-0001
---en Windows---
COM1
COM3
qph
Esto nos dice que en el listado de puertos sí está contemplando nuestro dispositivo. En el caso de Mac o Linux nuestro dispositivo sería el marcado con cu.usbserial-0001 y el tty.usbserial-0001 (son dos diferentes protocolos de acceder al mismo dispositivo).
El siguiente paso es abrir la comunicación con ese puerto en concreto. Existen tres diferentes tipos de comunicación, con bloqueo, con semi-bloqueo y la comunicación con bloqueo completo. En nuestro caso, ya que no debería de existir ningún otro dispositivo leyendo el flujo de bytes y queremos establecer la comunicación más estable y rápida posible usaremos comunicación con bloqueo completo. Esto significa que solo nosotros tendremos acceso a esta comuncación.
Según la documentación, si queremos abrir un puerto del cual leer un flujo de datos tenemos que usar el siguiente código ejemplo:
SerialPort comPort = SerialPort.getCommPorts()[0];
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
InputStream in = comPort.getInputStream();
try
{
for (int j = 0; j < 1000; ++j)
System.out.print((char)in.read());
in.close();
} catch (Exception e) { e.printStackTrace(); }
comPort.closePort();
El código ejemplo recibe una cadena de bytes y las imprime como caracteres. En nuestro caso nosotros no queremos hacer eso, sin embargo, podemos imprimirlos en binario y revisar si en efecto los valores que leemos son los valores que esperamos que esté enviando la tarjeta ESP-32.
Modificamos nuestro código original:
public static void main(String[] args) {
//Hay que definir cuál va a ser nuestro puerto
//Lo podemos definir a partir de la lista que vimos antes
//O lo podemos detectar por medio de entrada de usuario
int numPort = 7;
//Imprimimos la lista completa de puertos
SerialPort[] puertos = SerialPort.getCommPorts();
for(SerialPort p: puertos){
System.out.println(p.getSystemPortName());
}
SerialPort comPort = SerialPort.getCommPorts()[numPort];
comPort.setBaudRate(921600); //Máxima velocidad estable
comPort.openPort();
comPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
InputStream in = comPort.getInputStream();
//Aquí guardaremos el número conforme lo decodifiquemos
int numDecodificado = 0;
//Ciclo de lectura e interpretación
while(true) {
try {
//Este valor es de una sola lectura (un solo byte acomodado en un entero)
//La cola inicial del número es de puros ceros.
int lectura = in.read();
//Revisamos el primer bit para ver si es inicio de paquete
if((lectura & 0b1000_0000) == 0b1000_0000) {
//Como sabemos que este es un nuevo inicio de paquete,
//limpiamos el entero donde guardamos la decodificación
numDecodificado = 0;
//revisamos el segundo bit para ver si es negativo
if ((lectura & 0b1100_0000) == 0b1100_0000) {
//Como es un número negativo, tenemos que llenar de 1's
//esto para simular el complemento a 2
numDecodificado = 0xFFFF_F000;
}
lectura &= 0b0011_1111;
numDecodificado += lectura << 6;
}
//Sino es el primer byte, esperamos lectura 0b0sxx_xxxx
//Trabajando en el segundo byte
else {
lectura &= 0b0011_1111;
numDecodificado += lectura;
//Imprimimos cuando acaba el paquete
System.out.println(Integer.toBinaryString(numDecodificado));
}
} catch (Exception e) {
e.printStackTrace();
break;
}
}
comPort.closePort();
}
El paso siguiente es la conversión de valores del ADC -4096 -> 4095 a valores de voltaje usando las fórmulas que obtuvimos en la fase del diseño de circuito.
Agregamos una manera de calcular el voltaje en RN a partir del valor de la diferencia de los valores entre ambos ADCs y a partir de este valor obtenemos el valor de voltaje en T:
public static double VTdeVR(double resol, double deltaADC, double VRef, double R1, double R2, double N){
//Calculamos el valor del voltaje en RN
double VRN = VRef * deltaADC /resol;
//Regresamos el valor de V_T según la función inversa que calculamos desde el circuito
return VRN*(R1 * R2 + N * R1 + N * R2)/(R1 * R2);
}
Y agregamos la impresión de este método para visualizar los valores:
//Sino, esperamos lectura 0b0sxx_xxxx
//Trabajando en el segundo byte
else {
lectura &= 0b0011_1111;
numDecodificado += lectura;
//Imprimimos cuando acaba el paquete
System.out.println("Lectura ADC : "+ numDecodificado);
System.out.println("Voltaje V_RN: "+ VTdeVR(4096.0, numDecodificado, 3.3, 217.8, 233.2, 3300.0));
}
Podemos agregar una arreglo con valores de prueba para hacer pruebas y verificar que los valores obtenidos sean los correctos:
Por ejemplo, si en el arreglo introdujéramos los valores [0, 10, 1000, 4095, -1, -10, -100, -1000, -4096] obtendremos los valores como esperaríamos obtenerlos.
cu.wlan-debug
tty.wlan-debug
cu.ESP32_LED_Control
tty.ESP32_LED_Control
cu.Bluetooth-Incoming-Port
tty.Bluetooth-Incoming-Port
cu.usbserial-0001
tty.usbserial-0001
Lectura ADC : 0
Voltaje V_RN: -9.43396226419055E-4
Lectura ADC : 10
Voltaje V_RN: 0.12112461306013829
Lectura ADC : 100
Voltaje V_RN: 1.2197366966391452
Lectura ADC : 255
Voltaje V_RN: 3.111790840580775
Lectura ADC : 1000
Voltaje V_RN: 12.205857532429242
Lectura ADC : 4095
Voltaje V_RN: 49.98590640661851
Lectura ADC : -1
Voltaje V_RN: -0.013150197155072955
Lectura ADC : -10
Voltaje V_RN: -0.12301140551297639
Lectura ADC : -100
Voltaje V_RN: -1.2216234890919833
Lectura ADC : -1000
Voltaje V_RN: -12.20774432488208
Lectura ADC : -4096
Voltaje V_RN: -50.0
Los que son realmente importantes son los valores 0, 4095 y -4096 ya que estos valores son los que señalarán los valores de V_cero, V_Max, V_Min.
Podemos ver que los valores que obtuvimos coinciden con la predicción que habíamos realizado. Al tener la lectura del ADC más baja obtuvimos el valor de voltaje que deseábamos desde la fase de diseño, -50[V]. Al obtener el valor más alto, obtuvimos +50, que era el máximo valor que queríamos medir. Lo mismo sucede con el valor cuando el valor del ADC es 0, obtenemos un valor pequeñísimo muy muy cercano a cero.
Hay que recordar, aquí no estamos recibiendo los valores del ADC directamente sino que estamos recibiendo el valor de la resta de las lecturas de los dos ADCs.
Con este pequeño programa podríamos enviar el flujo de datos a cualquier aplicación gráfica por medio de protocolos del sistema operativo, sin embargo, por facilidad de programación, ya que estamos trabajando en un programa de Java, podremos agregar esa interfaz gráfica a este mismo programa.
Cómo haremos el proyecto
Ya que la aplicación se utilizará principalmente en computadoras personales (por diversas cuestiones convenientes como el uso de múltiples puertos USB y por extensión la posibilidad de usar varias tarjetas ESP-32) podemos pensar que en realidad el diseño del código que implementamos previamente es únicamente para interpretar los valores de un dispositivo cualquiera.
Podemos considerar que existen varios puertos USB en una computadora de la misma forma que existen otros puertos por los cuales podemos interpretar un valor de voltaje (por ejemplo la entrada de audio auxiliar). Pensemos que existen varios dispositivos conectados a la computadora. Inlcuso podríamos considerar la existencia de un "generador de señal" virtual, el cual genere una onda perfecta sinusoidal, una señal de escalón o simplemente un impulso cada determinado tiempo.
La solución que propongo yo para el manejo de todos estos dispositivos, y dejar abierta la posibilidad al desarrollo de dispositivos posteriores (como el uso de tarjetas integradas distintas a futuro así como generadores de señal personalizados) es manejar todos los valores que se introduzcan al osciloscopio por medio del protocolo UDP.
En este sentido, el osciloscopio, el intérprete de señal desde la ESP-32 así como cualquier dispositivo adicional posterior será una especia de aplicación independiente por sí sola. Esto nos dará ciertas ventajas a posteriori, no estaremos sujetos al desarrollo de ninguna aplicación por medio de ningún lenguaje de programación en específico. Además esto nos permitirá desarrollar incluso otros programas sustituto al osciloscopio así como de cada uno de los orígenes de señal que aquí hemos propuesto.
Implementación final
Los programas en los que se muestra el funcionamiento básico son los siguientes tres:
La arquitectura final de diseño será como en el siguiente diagrama:
En ánimos de darle modularidad a este proyecto, así como permitir que cualquiera pueda crear programas adicionales para su funcionamiento, el proyecto estará dividido en dos partes: generadores de señal y un graficador.
El graficador será un programa con interfaz gráfica de usuario donde se podrán visualizar los valores de voltaje a lo largo del tiempo, tal y como lo haríamos con un osciloscopio común. Estos valores de voltaje tendrán que llegar por medio de algún medio. Este medio deberá ser un medio accesible por cualquier otro programa que otras personas quieran programar. Es por esto que se optó por usar el protocolo UDP por medio de un puerto red para recibir los valores en el graficador.
Los generadores de señales serán únicamente programas que mandarán valores de voltaje a esos puertos UDP. En concreto, vamos a enviar un valor de 8 bytes entero "long" (el tiempo en nanosegundos cuando se generó ese voltaje) y el valor del voltaje que se generó "double".
Los valores del tiempo en nanosegundos se usarán para calcular el eje X en el que se graficará el valor de voltaje y los valores de voltaje para calcular el eje Y.
Uno de estos generadores de señal será el traductor de la ESP-32 a UDP. Este progama lo que hará será usar los valores de voltaje suministrados por el ADC de la tarjeta ESP-32 que llegarán por medio del puerto de monitor serial. Al tomar esos valores mandará esos valores por medio del protocolo UDP para que se reciban en el programa graficador.
Es importantísimo enfatizar, no es necesario utilizar todos los programas que estoy aquí suministrando para usar los demás. Es decir, si un usuario encontrara que mi programa generador de función seno es poco intuitiva, lenta o carente de funcionalidad, este usuario podría programar su propio programa para generar señales. El único verdader requisito sería mandar los valores "long" de tiempo y "double" de voltaje por los puertos UDP correctos.
Lo mismo ocurre con el programa graficador; el programa que estoy aquí presentando es un programa demostración con utilidad suficiente para muchos escenarios prácticos de los estudiantes, sin embargo, si un estudiante quisiera mejorarlo podría tomar su código fuente para agregarle las funcionalidades que considere faltantes, modificarlo más a su gusto o incluso reprogramarlo completamente y crear su propia implementación. Bajo ninguna circunstancia mi diseño es el mejor, hay mucho espacio para mejoría pero conceputalmente con este trabajo queda demostrado que se puede obtener la funcionalidad de un osciloscopio a partir de una computadora personal y una tarjeta de desarrollo si se usa el circuito y la programación correctas.
Omitiré las explicaciones del código en este documento.
Conclusiones
Invito a los usuarios de este proyecto a desarollar sus propios generadores de función y realizar sus modificaciones al programa graficador. Aquí he presentado los tres programas que he considerado más vitales para su uso: un programa que nos permite realizar mediciones con la ESP-32; un programa para generar funciones de referencia sinusoidales; un programa graficador para esas señales. Sin embargo, existen muchos programas que se podrían crear a partir de estas plantillas básicas.
- Generador de función escalón
- Generador de función sierra
- Generador de funciones arbitrarias a partir de un modelo
- Conección con el micrófono de la computadora
También existen cuestiones que no contempla mi graficador que se podrían agregar.
- Cálculo de frecuencias más frecuentes a partir de la transformada de Fourier
- Cálculo y correcta representación del momento "trigger" para centrar señales de manera estática
- Guardado y reproducción de señales personalizadas (i.e. a partir de archivos de audio)
Las únicas consideraciones a tener es la velocidad límite del protocolo UDP. De manera experimental logré determinar que mandar 50'000 lecturas (50'000 longs y 50'000 doubles) por segundo era suficientemente rápido como para empezar a ser poco confiable esto nos pone una cota superior a la máxima frecuencia confiable en 50kHz. Este fue el caso haciendo uso de mi computadora y mis circunstancias específicas de diseño. Quizá alguien con más conocimientos en este campo consiga velocidades más altas. Igualmente, con el paso del tiempo y el gradual incremento en las capacidades de las computadoras, quizá esta velocidad aumente con el tiempo también.
Suministraré aquí las plantillas básicas para aquellos que quieran programar una nueva señal sin crear un programa con interfaz gráfica de cero. Los comentarios en el código funcionarán de guía así como el documento de presentación visible en Github. Como siempre, recomiendo el uso del IDE Intellij Idea Community, pero cualquier otro editor de código sería igualmente funcional.
- Implementación sencilla: Plantilla generador de señal sin GUI
- Implementación completa: Plantilla generador de señal con GUI
Correo desfase
Buenas tardes profesor,
Le mando el correo únicamente a manera de reportar los avances que ha habido con respecto al tema que le contaba del desfase entre los relojes entre ambos ESPs.
Al momento esta ha sido la lectura más representativa que he conseguido. Es una lectura de voltaje al momento de pulsar un botón ambos yendo a la punta de lectura negativa de los ESPs.
Claramente podemos ver que el ESP rojo "reportó" el pico de voltaje antes que el verde, eso se debe al tema del "buffer" de bytes que no se ha leído en el verde. Este ha sido uno de los mejores comportamientos que he obtenido, teniendo una rápida velocidad de escritura haciendo un "flush" de los bytes de ese buffer cada segundo. El flush se realiza desde el programa de Java por lo que imagino que debe mandar un comando de borrado desde el puerto TX del puerto USB y el ESP-32 lo debe de recibir desde el RX del microUSB. Esta purga de bytes se hace cada segundo ya que es necesario consultar cuántos bytes están disponibles para lectura. Ese sondeo lo realizo cada segundo. Posteriormente, si el número de bytes acumulados es mayor a un valor arbitrario (encontré los mejores resultados en 1000), entonces se le solicita a ambos puertos vaciar sus buffers.
Sin embargo, dado que el desfase entre cada ESP crece en todo segundo, es una solución que nos brinda una mayor mejoría en las lecturas más cercanas a ese punto de reinicio de buffers pero va empeorando conforme pasa el tiempo (hasta pasar un segundo, donde se repite el ciclo).
Otra cosa que cabe destacar de la observación es que el valor de los voltajes es ligeramente mayor (en dimensión) en los valores de la ESP marcada en verde. En realidad es indistinto cual sea cual, lo que se puede apreciar es que la lectura, a pesar de venir de exáctamente la misma señal y tener ambos la tierra interconectada, los valores de lectura son diferentes para ambos ESPs.
He pensado en un par de soluciones, dado que el código de Arduino es sencillo no debería haber ninguna complicación en mantenerlo y hacer las siguientes modificaciones.
Cambiar la lectura desde la aplicación de Java para que se haga con dos diferentes hilos de ejecución. En este momento se realizan las lecturas en serie ya que fue la mejor manera de mantener el dibujo de las gráficas consistentes. Al usar hilos de ejecución se obtenían cosas raras en las iteraciones del proyecto pasadas. Sin embargo, con tanto hacer y rehacer, romper y arreglar el proyecto, creo que en este momento podría trabajarlo con hilos de ejecución mejor que en esas otras ocasiones. Para lograr este arreglo, sin embargo, probablemente me tomaría al menos una semana o quizá un par para reestructurar todo el proyecto.
Otra opción es cambiar la tarjeta recomendada por un Arduino Nano, entiendo que tiene un pin dedicado para la sincronización por lo que quizá sea la mejor opción.
La última opción contemplada hasta el momento (recomendada por una amistad) es es otra vez reestructurar el sistema de lecturas para que se guarde cada lectura con una etiqueta de tiempo. Esta última no estoy seguro cómo implementarla ya que tendría que deshacerme de alguna manera del buffer de bytes igualmente pero posteriormente el manejo de los datos sería ya con información al respecto de cada lectura.
Por el momento dejaré (aunque sea con el desfase que tiene) el código como está e intentaré ver cómo calibrarlo haciendo uso de un generador de señales.
Espero a sus comentarios AMR
Correo condensadores
Buenas tardes profesor,
Le comunicaré brevemente los avances conseguidos al hacer las mediciones, calibraciones y modificaciones que consideré pertinentes durante las últimas semanas.
Pude utilizar un generador de funciones y un osciloscopio como referencia por lo que teniendo esa referencia tengo suficiente confianza en los valores obtenidos.
Lo primero realizado fue la verificación de las mediciones del voltaje en corriente directa; las mediciones obtenidas en directa fueron las mismas que se realizaron durante la última presentación que pudimos realizar en Zoom.
Lo segundo realizado fue la verificación de las señales de alterna; estas debido a como estaba referenciado el circuito, no se obtenían las mediciones correctas una vez el voltaje introducido a la medición era demasiado alto por lo que se tuvo que replantear el circuito usado para la medición. Después de reformular y también reformular una alternativa viable, con ambos nuevos circuitos (los cuales llamaré "en serie" y "en paralelo" cuyos nombres serán más claros la próxima presentación que tenga con usted). Usando ambos circuitos pude obtener los valores correctos de la medición del delta de voltaje entre las dos terminales de voltaje con los que se había establecido que se realizaría la medición.
En ambos casos los valores obtenidos eran claramente visibles cuando la señal enviada era de una frecuencia igual o menor a 1kHz, el valor que en su momento habíamos planteado como límite máximo en su momento. En valores posteriores, 2kHz todavía se podía obtener una imagen aunque ya malformada de la señal seno que se estaba enviando. Frecuencias posteriores daban únicamente ruido.
El circuito en serie y el circuito en paralelo ambos resultan suficientes para realizar las mediciones con sus respectivas ventajas en diferentes casos de uso; para hacer cálculos con rápidamente y cambiar los resistores usados resulta más fácil trabajar con el circuito en serie mientras que el circuito en paralelo, aunque requiere más talacha para obtener los resistores adecuados, requiere resistores con menor disipación de potencia ya que la medición se realiza con resistores grandes mientras que con el arreglo en serie se realizan con los resistores más chicos.
Por último, la medición realizada en ambos circuitos resultó cercana a los valores anticipados por la teoría realizada en papel (y posteriormente en simulador). En ambos casos se obtuvieron valores cercanos a los de la teoría en la medición, sin embargo, dado el ruido también leído, sí existió algo de desviación. Consideré buscar un factor de ajuste con el cual se contemplaran los errores de manufactura de los resistores así como la resistencia (desconocida pero bastante alta) del pin GPIO en el cual se estaba realizando la medición, sin embargo, dado que la desviación entre la medición del osciloscopio y los circuitos en los que empleamos el ESP-32 era en cantidades semejantes "para arriba" y "para abajo", decidí que sería mejor postergar hasta consultarlo con usted.
En cuanto a lo que habíamos platicado de la posibilidad de diferentes escalas para la reducción de la onda medida previo a introducirlo al ESP-32, concluí que la mejor opción es probablemente el armado de diferentes resistores a priori, para que la medición realizada se introduzca o se obtenga de un arreglo de resistores ya prearmado. (Espero poder armar una simulación de esta idea esta semana para dejarlo más claro que solo con palabras.) Por último, estoy reconsiderando la posibilidad de agregar diodos Zener para agregar un factor de seguridad a la entrada de los pines GPIO.
Espero a sus comentarios,
AMR
Correo reporte uso baterías
Correo protocolo señal
Buenas tardes profesor,
Fui a su cubículo el día de hoy a las 11 del día y posteriormente a las 12 pero no se encontraba. Dado que tenía clase a me tuve que retirar pero le mando los avances que hubo hasta el día de hoy.
Pude asistir con la profesora Yoloxóchitl para la inspección del osciloscopio (aunque por ciertas circunstancias solo pudimos observar el comportamiento del digital). Hice bastantes anotaciones sobre qué tipo de comportamiento uno podría esperar de un osciloscopio digital de entre los cuales podría resaltar los siguientes:
La grafica que se muestra en el osciloscopio del laboratorio puede cambiar de modo, los dos modos que le mencioné la semana pasada; graficación continua y graficación por sección; ambos los puede realizar el osciloscopio que tienen ahí. Es por eso que creo que sería conveniente implementar ambos métodos. La graficación por secciones hace un truncamiento de buena parte de la señal leída y únicamente muestra una parte "en el centro" del arreglo de los valores leídos de forma que se pueden ver los datos bastante filtrados sin temer a que los valores de las esquinas sean picos o valles de la señal sino que todo se centra en algún punto en el centro donde se cumplan los requisitos solicitados por la configuración. Entre estos requisitos están 1. Cruce del voltaje de tierra o referencia y 2. Voltaje ascendente o descendiente. Según cuál de éstos elija uno, el osciloscopio los grafica.
Otra cosa muy interesante que encontré es que en los osciloscopios digitales es mucho más latoso de lo que anticipaba hacer graficación de señales teniendo dos canales conectados, uno como eje X y uno como eje Y en el monitor. En el osciloscopio analógico recuerdo que era muy fácil entrar en este modo para graficar círculos, cuadrados o líneas según el tipo de señal que uno le estuviera introduciendo.
Otras ventajas como guardar los valores leídos (toda la señal a lo largo del tiempo) captura de imágenes de la lectura etc. son bondades que también están presentes en el osciloscopio digital y no creo que sea difícil implementarlas en el programa del osciloscopio en la computadora ya que contamos con toda la infraestructura del sistema operativo para la manipulación de archivos etc.
Como avance paralelo me puse a escribir la el manual y la demostración del funcionamiento del osciloscopio como lo tenía pensado hasta el momento, sin embargo, encontré un problema que no habíamos contemplado. Al hacer lecturas de 256 valores (8 bits) hasta el momento únicamente se pueden leer valores de voltaje positivos ya que la resta de los voltajes se hace en la ESP-32 y luego se envía el byte a la computadora. Pensé en dos posibles soluciones:
-
Multiplicar los valores de la lectura de la punta positiva por 2 para así tener un rango de 0 -> 511 y así al restar el voltaje negativo sin miedo a que se presenten valores "negativos" en el short. Posteriormente dividir el resultado entre dos (regresándolo a una escala 0->255) y luego mandarlo a la computadora.
-
Regresar a la propuesta de usar dos bytes para cada lectura cambiando un poco el arreglo de los bits por cada byte. En vez de mandar 0F FF como valor máximo, mandar un poco de información adicional. Si la lectura fuera de 4095 (lectura máxima) mandarla como 0011 1111 1011 1111 (2F BF) reservando el espacio del primer bit como 0 para el primer byte leído y como 1 para el segundo byte leído. El 0 que queda en sin usarse (en la segunda posición más significativa) considero que sería un buen lugar para guardar información como "positivo" o "negativo"; siendo 0 positivo y 1 negativo.
Así, si la lectura de voltaje fuera -4095 el valor enviado sería 01'111111 11'111111 (7F, FF) donde el valor real de la señal se encontraría en los últimos seis bits de cada byte y los dos primeros serían información de "primer bit, segundo bit, negativo, negativo" El bit de negativo/positivo tiene redundancia al mandarlo dos veces pero no creo que haya más información de interés que se pueda mandar en ese bit (al menos hasta el momento).
Intentaré la implementación de ambos para ver qué velocidades de lectura resultan de estos cambios en la lectura de los valores y también intentaré seguir con la implementación del nuevo programa.
Hasta el momento creo que no ha habido más avances pero espero a sus comentarios.
AMR
programa-arduino.ino
Presento aquí el código en Arduino para su fácil acceso
//El número de pin en que esperaremos leer los valores de voltaje
#define pin_ADC_A 4
#define pin_ADC_B 15
//Usaremos short que es una estructura de 16 bits
int lectura_ADC_A = 0b0;
int lectura_ADC_B = 0b0;
int resta_ADC = 0b0;
//Estas serán las variables que enviaremos por medio del serial
byte num_codificado_primero = 0b0;
byte num_codificado_segundo = 0b0;
void setup() {
Serial.begin(921600);
}
void loop() {
//Inicializamos los valores de nuestros bytes
//Podemos poner el etiquetado del primero byte de una vez
num_codificado_primero = 0b10000000;
num_codificado_segundo = 0b00000000;
lectura_ADC_A = analogRead(pin_ADC_A);
lectura_ADC_B = analogRead(pin_ADC_B);
resta_ADC = lectura_ADC_A - lectura_ADC_B;
//Recordemos que resta_ADC es un número de 16 bits
//Nosotros trabajamos con dos bytes separados
//Por esto tenemos que recorrer el entero 8 bits
//Así tenemos acceso a la cola de desbordamiento 1111 (ó 0000)
//Aquí estamos sumamos el segundo bit, donde va el signo
//Por eso aplicamos una máscara lógica & 0b0100
num_codificado_primero += ((resta_ADC >> 8) & 0b01000000);
num_codificado_segundo += ((resta_ADC >> 8) & 0b01000000);
//Aplicando otra máscara lógica sumamos los 6 bits
//Tenemos que recorrer 6 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxxx_xxxx
// isxx_xxxx -> is_xxxx_xx
num_codificado_primero += ((resta_ADC >> 6) & 0b00111111);
//Tenemos que recorrer 0 lugares para sumar al primer byte
// ssss_xxxx_xxxx_xxxx -> ssss_xxxx_xxyy_yyyy
// isxx_xxxx -> isxx_xxxx
num_codificado_segundo += (resta_ADC & 0b00111111);
Serial.write(num_codificado_primero);
Serial.write(num_codificado_segundo);
}