Girino - Osciloscopio Arduino Rápido

Soy físico y la mejor parte de trabajar en este campo es que puedo construir mis propios instrumentos. Con esta forma de pensar, decidí construir un osciloscopio Arduino casero. Este instructable fue escrito con el propósito de enseñar un poco sobre microcontroladores y adquisición de datos. Este es un proyecto extremo porque quería exprimir de Arduino tanta velocidad como pude, no he visto ningún otro osciloscopio Arduino tan rápido como este.

Hace algún tiempo estaba trabajando en un proyecto Arduino y necesitaba ver si la señal de salida cumplía con los detalles. Por lo tanto, pasé un tiempo en Internet buscando los osciloscopios Arduino ya implementados, pero no me gustó lo que encontré. Los proyectos que encontré estaban compuestos principalmente por una interfaz gráfica de usuario para la computadora escrita en Processing y un boceto arduino muy simple. Los bocetos eran algo así como: void setup () {

Serial.begin (9600);

}

bucle vacío () {

int val = analogRead (ANALOG_IN);

Serial.println (val);

} Este enfoque no está mal y no quiero insultar a nadie, pero esto es demasiado lento para mí. El puerto serie es lento y enviar todos los resultados de un analogRead () a través de él es un cuello de botella.

He estado estudiando Waveform Digitizers durante algún tiempo y sé bastante bien cómo funcionan, así que me inspiré en ellos. Estos fueron los puntos de partida del osciloscopio que quería crear:

  • la señal entrante se debe desacoplar del arduino para preservarla;
  • con un desplazamiento de la señal es posible ver señales negativas;
  • los datos deben ser almacenados temporalmente;
  • se requiere un disparador de hardware para captar las señales;
  • un búfer circular puede dar forma a la señal antes del disparo (más a seguir en este punto);
  • El uso de funciones de palanca más bajas que las estándar hace que el programa se ejecute más rápido.

El boceto del Arduino se adjunta a este paso, junto con el esquema del circuito que hice.

El nombre que se me ocurrió, Girino, es un juego de palabras frívolo en italiano. Giro significa rotación y agregando el sufijo -ino obtienes una pequeña rotación, pero Girino también significa renacuajo . De esta manera obtuve un nombre y una mascota.

Paso 1: Descargo de responsabilidad

EL AUTOR DE ESTE INSTRUCTABLE NO GARANTIZA LA VALIDEZ NI GARANTÍA DE NINGÚN TIPO .

La electrónica puede ser peligrosa si no sabe lo que está haciendo y el autor no puede garantizar la validez de la información que se encuentra aquí. Este no es un consejo profesional y cualquier cosa escrita en este instructivo puede ser inexacta, engañosa, peligrosa o incorrecta. No confíe en ninguna información encontrada aquí sin una verificación independiente.

Depende de usted verificar cualquier información y verificar que no se exponga a usted ni a nadie a ningún daño ni exponga nada a ningún daño; No me hago responsable. Debe seguir las precauciones de seguridad adecuadas si desea reproducir este proyecto.

¡Use esta guía bajo su propio riesgo!

Paso 2: lo que necesitas

Lo que realmente necesitamos para este proyecto es una placa Arduino y la hoja de datos del ATMega328P.
La hoja de datos es lo que nos dice cómo funciona el microcontrolador y es muy importante mantenerlo si queremos una palanca de control más baja.

La hoja de datos se puede encontrar aquí: //www.atmel.com/Images/doc8271.pdf

El hardware que agregué al Arduino es en parte necesario, su propósito es solo formar la señal para el ADC y proporcionar un nivel de voltaje para el disparador. Si lo desea, puede enviar la señal directamente al Arduino y usar alguna referencia de voltaje definida por un divisor de voltaje, o incluso los 3.3 V dados por el propio Arduino.

Paso 3: salida de depuración

Normalmente pongo una gran cantidad de resultados de depuración en mis programas porque quiero hacer un seguimiento de todo lo que sucede; El problema con Arduino es que no tenemos un stdout para escribir. Decidí usar el puerto serie como stdout.

Sin embargo, tenga en cuenta que este enfoque no funciona todo el tiempo. Porque escribir en el puerto serie requiere algo de tiempo para la ejecución y puede cambiar drásticamente las cosas durante algún tiempo en las rutinas razonables.

Por lo general, defino las salidas de depuración dentro de una macro de preprocesador, por lo que cuando la depuración está deshabilitada, simplemente desaparecen del programa y no ralentizan la ejecución:
  • dprint (x); - Escribe en el puerto serie algo como: # x: 123
  • dshow ("Alguna cadena"); - Escribe la cadena

Esta es la definición:

#if DEBUG == 1
#define dprint (expresión) Serial.print ("#"); Serial.print (# expresión); Serial.print (":"); Serial.println (expresión)
#define dshow (expresión) Serial.println (expresión)
#más
#define dprint (expresión)
#define dshow (expresión)
#terminara si

Paso 4: Configuración de bits de registro

Con el fin de ser rápido, es necesario manipular las funciones del microcontrolador con funciones de palanca más bajas que las estándar proporcionadas por el IDE Arduino. Las funciones internas se gestionan a través de algunos registros, que son colecciones de ocho bits donde cada uno gobierna algo en particular. Cada registro contiene ocho bits porque el ATMega328P tiene una arquitectura de 8 bits.

Los registros tienen algunos nombres que se especifican en la hoja de datos dependiendo de sus significados, como ADCSRA para el registro de configuración ADC A. Además, cada bit significativo de los registros tiene un nombre, como ADEN para el bit de habilitación ADC en el registro ADCSRA.

Para establecer sus bits, podríamos usar la sintaxis C habitual para el álgebra binaria, pero encontré en Internet un par de macros que son muy agradables y limpias:

// Define para establecer y borrar bits de registro
#ifndef cbi
#define cbi (sfr, bit) (_SFR_BYTE (sfr) & = ~ _BV (bit))
#terminara si
#ifndef sbi
#define sbi (sfr, bit) (_SFR_BYTE (sfr) | = _BV (bit))
#terminara si

Usarlos es muy simple, si queremos establecer en 1 el bit de habilitación del ADC, simplemente podemos escribir:

sbi (ADCSRA, ADEN);

Mientras que si queremos establecerlo en 0 ( id está claro) podemos simplemente escribir:

cbi (ADCSRA, ADEN);

Paso 5: ¿Cuáles son las interrupciones?

Como veremos en los próximos pasos, se requiere el uso de interrupciones en este proyecto. Las interrupciones son señales que le indican al microcontrolador que detenga la ejecución del bucle principal y lo pase a algunas funciones especiales. Las imágenes dan una idea del flujo del programa.

Las funciones que se ejecutan se denominan Rutinas de servicio de interrupción (ISR) y son funciones más o menos simples, pero que no toman argumentos.

Veamos un ejemplo, algo así como contar algunos pulsos. El ATMega328P tiene un comparador analógico que tiene una interrupción asociada que se activa cuando una señal supera un voltaje de referencia. En primer lugar, debe definir la función que se ejecutará:

ISR (ANALOG_COMP_vect)
{
contador ++;
}

Esto es realmente simple, la instrucción ISR () es una macro que le dice al compilador que la siguiente función es una Rutina de servicio de interrupción. Mientras ANALOG_COMP_vect se llama Vector de interrupción y le dice al compilador qué interrupción está asociada a esa rutina. En este caso es la interrupción del comparador analógico. Entonces, cada vez que el comparador ve una señal más grande que una referencia, le dice al microcontrolador que ejecute ese código, id en este caso para incrementar esa variable.

El siguiente paso es habilitar la interrupción asociada. Para habilitarlo, debemos establecer el bit ACIE (Activación de interrupción del comparador analógico) del registro ACSR (Registro de configuración del comparador analógico):

sbi (ACSR, ACIE);

En el siguiente sitio podemos ver la lista de todos los vectores de interrupción:
//www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

Paso 6: Adquiera continuamente con un búfer circular

El concepto de usar un Buffer Circular es bastante sencillo:

Adquiera continuamente hasta que se encuentre una señal, luego envíe la señal digitalizada a la computadora.

Este enfoque permite tener la forma de la señal entrante también antes del evento de activación.


Preparé algunos diagramas para aclararme. Los siguientes puntos se refieren a las imágenes.
  • En la primera imagen podemos ver lo que quiero decir con adquisición continua . Definimos un búfer que almacenará los datos, en mi caso, una matriz con 1280 ranuras, luego comenzamos a leer continuamente el anuncio de registro de salida ADC (ADCH) que llena el búfer con los datos. Cuando llegamos al final del búfer, reiniciamos desde el principio sin borrarlo. Si imaginamos la matriz organizada de forma circular, es fácil ver a qué me refiero.
  • Cuando la señal supera el umbral, se activa la interrupción del comparador analógico. Luego comenzamos una fase de espera en la que continuamos adquiriendo la señal pero contamos los ciclos de ADC que pasaron desde la Interrupción del Comparador Analógico.
  • Cuando esperamos N ciclos (con N <1280), congelamos la situación y detenemos los ciclos de ADC. Entonces terminamos con un buffer lleno con la digitalización de la forma temporal de la señal. La gran parte de esto es que también tenemos la forma previa al evento desencadenante, porque ya estábamos adquiriendo antes de eso.
  • Ahora podemos enviar todo el búfer al puerto serie en un bloque de datos binarios, en lugar de enviar las lecturas de ADC individuales. Esto redujo la sobrecarga requerida para enviar los datos y el cuello de botella de los bocetos que encontré en Internet.

Paso 7: activación del osciloscopio

Un osciloscopio muestra en su pantalla una señal, en la que todos estamos de acuerdo, pero ¿cómo puede mostrarlo de manera constante y no mostrarlo saltando por la pantalla? Tiene un disparador interno que puede mostrar la señal siempre en la misma posición de la pantalla (o al menos la mayoría de las veces), creando la ilusión de una trama estable.

El disparador está asociado con un umbral que activa un barrido cuando la señal lo pasa. Un barrido es la fase en la que el osciloscopio registra y muestra la señal. Después de un barrido se produce otra fase: la retención, en la cual el osciloscopio rechaza cualquier señal entrante. El período de espera puede estar compuesto por una parte de tiempo muerto, en el que el osciloscopio no puede aceptar ninguna señal, y una parte que puede ser seleccionada por el usuario. El tiempo muerto puede ser causado por varias razones, como tener que dibujar en la pantalla o tener que almacenar los datos en algún lugar.

Mirando la imagen tenemos una idea de lo que sucede.
  1. La señal 1 supera el umbral y activa el barrido;
  2. la señal 2 está dentro del tiempo de barrido y queda atrapada con la primera;
  3. después del retraso, la señal 3 activa nuevamente el barrido;
  4. en cambio, la señal 4 se rechaza porque cae dentro de la región de espera.
La razón de ser de la fase de espera es evitar que algunas señales no deseadas entren en la región de barrido. Es un poco largo explicar este punto y elude el propósito de este instructable.

La moraleja de esta historia es que necesitamos:
  1. un nivel de umbral con el cual podemos comparar la señal entrante;
  2. una señal que le dice al microcontrolador que inicie la fase de espera (vea el paso anterior).
Tenemos varias soluciones posibles para el punto 1.:
  • usando una podadora podemos establecer manualmente un nivel de voltaje;
  • usando el PWM del Arduino podemos establecer el nivel por software;
  • utilizando los 3.3 V proporcionados por el propio Arduino;
  • usando la referencia interna de bangap podemos usar un nivel fijo.
Para el punto 2. tenemos la solución correcta: podemos usar la interrupción del comparador analógico interno del microcontrolador.

Paso 8: Cómo funciona el ADC

El microcontrolador Arduino presenta un solo ADC de aproximación sucesiva de 10 bits. Antes del ADC hay un multiplexor analógico que nos permite enviar, al ADC, las señales de diferentes pines y fuentes (pero solo una a la vez).

El ADC de aproximación sucesiva significa que el ADC tarda 13 ciclos de reloj para completar la conversión (y 25 ciclos de reloj para la primera conversión). Hay una señal de reloj dedicada al ADC que se "calcula" desde el reloj principal del Arduino; Esto se debe a que el ADC es un poco lento y no puede seguir el ritmo de las otras partes del microcontrolador. Requiere una frecuencia de reloj de entrada entre 50 kHz y 200 kHz para obtener la máxima resolución. Si se necesita una resolución inferior a 10 bits, la frecuencia del reloj de entrada al ADC puede ser superior a 200 kHz para obtener una frecuencia de muestreo más alta.

Pero, ¿cuánto más altas tasas podemos usar? Hay un par de buenas guías sobre el ADC en los Open Music Labs que sugiero leer:
  • //www.openmusiclabs.com/learning/digital/atmega-adc/
  • //www.openmusiclabs.com/learning/digital/atmega-adc/in-depth/
Como mi propósito es obtener un osciloscopio rápido, decidí limitar la precisión a 8 bits. Esto tiene varias bonificaciones:
  1. el búfer de datos puede almacenar más datos;
  2. no desperdicies 6 bits de RAM por dato;
  3. El ADC puede adquirir más rápido.
El preescalador nos permite dividir la frecuencia, por algunos factores, al configurar los bits ADPS0-1-2 del registro ADCSRA. Al ver la trama de la precisión del artículo de Open Music Labs, podemos ver que para una precisión de 8 bits la frecuencia podría subir a 1.5 MHz, ¡bien! Pero dado que la capacidad de cambiar el factor de preescalador nos permite cambiar la velocidad de adquisición, también podemos usarla para cambiar la escala de tiempo del osciloscopio.

Hay una buena característica sobre los registros de salida: podemos decidir el ajuste de los bits de conversión configurando el bit ADLAR en el registro ADMUX. Si es 0, están correctamente ajustados y viceversa (ver la imagen). Como quería una precisión de 8 bits, lo configuré en 1 para poder leer solo el registro ADCH e ignorar el ADCL.

Decidí tener solo un canal de entrada para evitar tener que cambiar de canal de un lado a otro en cada conversión.

Una última cosa sobre el ADC, tiene diferentes modos de ejecución, cada uno con una fuente de disparo diferente:
  • Modo de carrera libre
  • Comparador analógico
  • Solicitud de interrupción externa 0
  • Temporizador / Contador0 Comparar Match A
  • Temporizador / Contador 0 Desbordamiento
  • Temporizador / Contador1 Comparar partido B
  • Temporizador / Contador1 Desbordamiento
  • Temporizador / Contador1 Evento de captura
Estaba interesado en el modo de ejecución libre que es un modo en el que el ADC convierte continuamente la entrada y lanza una interrupción al final de cada conversión (vector asociado: ADC_vect).

Paso 9: memorias intermedias de entrada digital

Los pines de entrada analógica del Arduino también se pueden usar como pines de E / S digitales, por lo tanto, tienen un búfer de entrada para funciones digitales. Si queremos usarlos como pines analógicos, debe desactivar esta función.

Enviar una señal analógica a un pin digital lo induce a alternar entre los estados ALTO y BAJO, especialmente si la señal está cerca del límite entre los dos estados; Esta alternancia induce algo de ruido a los circuitos cercanos como el ADC (e induce un mayor consumo de energía).

Para deshabilitar el búfer digital, debemos establecer los bits ADCnD del registro DIDR0:

sbi (DIDR0, ADC5D);
sbi (DIDR0, ADC4D);
sbi (DIDR0, ADC3D);
sbi (DIDR0, ADC2D);
sbi (DIDR0, ADC1D);
sbi (DIDR0, ADC0D);

Paso 10: Configuración del ADC

En el boceto, escribí una función de inicialización que configura todos los parámetros del funcionamiento del ADC. Como tiendo a escribir código limpio y comentado, simplemente pasaré la función aquí. Podemos referirnos al paso anterior y a los comentarios sobre el significado de los registros. nulo initADC (nulo)
= (ADCPIN & 0x07);

// ------------------------------------------------ ---------------------
// configuración de ADCSRA
// ------------------------------------------------ ---------------------
// Escribir este bit en uno habilita el ADC. Al escribirlo a cero, el
// ADC está desactivado. Apagar el ADC mientras hay una conversión
// progreso, terminará esta conversión.
cbi (ADCSRA, ADEN);
// En el modo de conversión única, escriba este bit en uno para comenzar cada
// conversión. En el modo de ejecución libre, escriba este bit en uno para iniciar el
// primera conversión. La primera conversión después de ADSC ha sido escrita
// después de que se haya habilitado el ADC, o si ADSC se escribe al mismo
// tiempo en que el ADC está habilitado, tomará 25 ciclos de reloj ADC en lugar de
// lo normal 13. Esta primera conversión realiza la inicialización de
// ADC. ADSC se leerá como uno solo mientras haya una conversión en curso.
// Cuando se completa la conversión, vuelve a cero. Escribiendo cero a
// este bit no tiene efecto.
cbi (ADCSRA, ADSC);
// Cuando este bit se escribe en uno, la activación automática del ADC es
// habilitado. El ADC comenzará una conversión en un borde positivo del
// señal de disparo seleccionada. La fuente de activación se selecciona configurando
// los bits de selección de disparador ADC, ADTS en ADCSRB.
sbi (ADCSRA, ADATE);
// Cuando este bit se escribe en uno y se establece el bit I en SREG, el
// Se activa la interrupción de conversión completa de ADC.
sbi (ADCSRA, ADIE);
// Estos bits determinan el factor de división entre el reloj del sistema
// frecuencia y el reloj de entrada al ADC.
// ADPS2 ADPS1 ADPS0 Factor de división
// 0 0 0 2
// 0 0 1 2
// 0 1 0 4
// 0 1 1 8
// 1 0 0 16
// 1 0 1 32
// 1 1 0 64
// 1 1 1 128
sbi (ADCSRA, ADPS2);
sbi (ADCSRA, ADPS1);
sbi (ADCSRA, ADPS0);

// ------------------------------------------------ ---------------------
// configuración de ADCSRB
// ------------------------------------------------ ---------------------
// Cuando este bit está escrito en lógica uno y el ADC está apagado
// (ADEN en ADCSRA es cero), el multiplexor ADC selecciona el negativo
// entrada al comparador analógico. Cuando este bit se escribe lógica cero,
// AIN1 se aplica a la entrada negativa del Analog Comparator.
cbi (ADCSRB, ACME);
// Si ADATE en ADCSRA se escribe en uno, el valor de estos bits
// selecciona qué fuente activará una conversión ADC. Si ADATE es
// borrado, la configuración ADTS2: 0 no tendrá efecto. Una conversión será
// se activará por el borde ascendente del indicador de interrupción seleccionado. Nota
// ese cambio de una fuente de activación que se borra a una activación
// fuente establecida, generará una ventaja positiva en el disparador
// señal. Si ADEN en ADCSRA está configurado, esto iniciará una conversión.
// Cambiar al modo de ejecución libre (ADTS [2: 0] = 0) no provocará un
// desencadenar evento, incluso si se establece el indicador de interrupción de ADC.
// ADTS2 ADTS1 ADTS0 Fuente de activación
// 0 0 0 Modo de ejecución libre
// 0 0 1 Comparador analógico
// 0 1 0 Solicitud de interrupción externa 0
// 0 1 1 Temporizador / Contador0 Comparar Match A
// 1 0 0 Temporizador / Contador0 Desbordamiento
// 1 0 1 Temporizador / Contador1 Comparar Match B
// 1 1 0 Temporizador / Contador1 Desbordamiento
// 1 1 1 Temporizador / Contador1 Evento de captura
cbi (ADCSRB, ADTS2);
cbi (ADCSRB, ADTS1);
cbi (ADCSRB, ADTS0);

// ------------------------------------------------ ---------------------
// configuración DIDR0
// ------------------------------------------------ ---------------------
// Cuando este bit se escribe lógica uno, el búfer de entrada digital en el
// el pin ADC correspondiente está deshabilitado. El registro PIN correspondiente
// bit siempre se leerá como cero cuando se establece este bit. Cuando un análogo
// la señal se aplica al pin ADC5..0 y la entrada digital de este
// no se necesita pin, este bit debe escribirse lógicamente uno para reducir
// consumo de energía en el búfer de entrada digital.
// Tenga en cuenta que los pines ADC ADC7 y ADC6 no tienen buffers de entrada digital,
// y, por lo tanto, no requieren bits de deshabilitación de entrada digital.
sbi (DIDR0, ADC5D);
sbi (DIDR0, ADC4D);
sbi (DIDR0, ADC3D);
sbi (DIDR0, ADC2D);
sbi (DIDR0, ADC1D);
sbi (DIDR0, ADC0D);

Paso 11: cómo funciona el comparador analógico

El Analog Comparator es un módulo interno del microcontrolador y compara los valores de entrada en el pin positivo (Pin digital 6) y el pin negativo (Pin digital 7). Cuando el voltaje en el pin positivo es mayor que el voltaje en el pin negativo AIN1, el comparador analógico genera un 1 en el bit ACO del registro ACSR.

Opcionalmente, el comparador puede activar una interrupción, exclusiva del comparador analógico. El vector asociado es ANALOG_COMP_vect.

También podemos configurar la interrupción para que se inicie en un borde ascendente, un borde descendente o en una palanca del estado.

El comparador analógico es justo lo que necesitamos para la activación de la conexión de la señal de entrada al pin 6, ahora lo que queda es un nivel de umbral en el pin 7.

Paso 12: Configuración del comparador analógico

En el boceto, escribí otra función de inicialización que configura todos los parámetros del funcionamiento del Analog Comparator. El mismo problema con los búferes digitales ADC se aplica al Analog Comparator, como podemos ver en la parte inferior de la rutina.

void initAnalogComparator (void)
{
// ------------------------------------------------ ---------------------
// configuración de ACSR
// ------------------------------------------------ ---------------------
// Cuando este bit está escrito en lógica uno, el poder del Analog
// El comparador está apagado. Este bit se puede configurar en cualquier momento para activar
// fuera del comparador analógico. Esto reducirá el consumo de energía en
// Modo activo e inactivo. Al cambiar el bit ACD, el Analog
// La interrupción del comparador debe deshabilitarse borrando el bit ACIE en
// ACSR. De lo contrario, puede producirse una interrupción cuando se cambia el bit.
cbi (ACSR, ACD);
// Cuando se establece este bit, un voltaje de referencia de banda prohibida fija reemplaza el
// entrada positiva al Analog Comparator. Cuando este bit se borra,
// AIN0 se aplica a la entrada positiva del Analog Comparator. Cuando
// la referencia de bandgap se usa como entrada para el Analog Comparator,
// tomará cierto tiempo para que el voltaje se estabilice. Si no
// estabilizado, la primera conversión puede dar un valor incorrecto.
cbi (ACSR, ACBG);
// Cuando el bit ACIE se escribe lógicamente uno y el bit I en el estado
// El registro está configurado, la interrupción del comparador analógico está activada.
// Cuando se escribe lógica cero, la interrupción está deshabilitada.
cbi (ACSR, ACIE);
// Cuando se escribe la lógica uno, este bit habilita la función de captura de entrada
// en Timer / Counter1 para ser activado por el Analog Comparator. los
// la salida del comparador está en este caso directamente conectada a la entrada
// captura la lógica frontal, haciendo que el comparador utilice el ruido
// características de cancelación y selección de borde de la entrada Timer / Counter1
// Captura interrupción. Cuando se escribe lógica cero, no hay conexión entre
// existe el comparador analógico y la función de captura de entrada. A
// hacer que el comparador active la captura de entrada del temporizador / contador1
// interrupción, el bit ICIE1 en el registro de máscara de interrupción del temporizador
// (TIMSK1) debe estar configurado.
cbi (ACSR, ACIC);
// Estos bits determinan qué eventos comparadores activan el Analog
// Interrupción del comparador.
// ACIS1 Modo ACIS0
// 0 0 alternar
// 0 1 Reservado
// 1 0 Borde descendente
// 1 1 Flanco ascendente
sbi (ACSR, ACIS1);
sbi (ACSR, ACIS0);

// ------------------------------------------------ ---------------------
// configuración de DIDR1
// ------------------------------------------------ ---------------------
// Cuando este bit se escribe lógica uno, el búfer de entrada digital en el
// El pin AIN1 / 0 está deshabilitado. El bit de registro PIN correspondiente será
// siempre se lee como cero cuando se establece este bit. Cuando una señal analógica es
// aplicado al pin AIN1 / 0 y la entrada digital de este pin no es
// necesario, este bit debe escribirse lógicamente uno para reducir la potencia
// consumo en el búfer de entrada digital.
sbi (DIDR1, AIN1D);
sbi (DIDR1, AIN0D);
}

Paso 13: umbral

Recordando lo que dijimos sobre el desencadenante, podemos implementar estas dos soluciones para el umbral:
  • usando una podadora podemos establecer manualmente un nivel de voltaje;
  • Usando el PWM del Arduino podemos establecer el nivel por software.
En la imagen podemos ver la implementación de hardware del umbral en ambas rutas.

Para la selección manual, un potenciómetro de múltiples vueltas puesto entre +5 V y GND es suficiente.

Mientras que para la selección de software necesitamos un filtro de paso bajo que filtre una señal PWM proveniente del Arduino. Las señales PWM (más sobre esto a continuación) son señales cuadradas con una frecuencia constante pero un ancho de pulso variable. Esta variabilidad trae un valor medio variable de la señal que se puede extraer con un filtro de paso bajo. Una buena frecuencia de corte para el filtro es aproximadamente una centésima parte de la frecuencia PWM y elegí unos 560 Hz.

Después de las dos fuentes de umbral, inserté un par de pines que permiten seleccionar, con un puente, qué fuente quería. Después de la selección, también agregué un seguidor de emisor para desacoplar las fuentes del pin Arduino.

Paso 14: Cómo funciona la modulación de ancho de pulso

Como se indicó anteriormente, una señal de modulación de ancho de pulso (PWM) es una señal cuadrada con frecuencia fija pero ancho variable. En la imagen vemos un ejemplo. En cada fila hay una de esas señales con un ciclo de trabajo diferente ( es decir, la parte del período en que la señal es Alta). Tomando la señal media durante un período, obtenemos la línea roja que corresponde al ciclo de trabajo con respecto al máximo de la señal.

Electrónicamente, "tomar la media de una señal" se puede traducir a "pasarla a un filtro de paso bajo", como se ve en el paso anterior.

¿Cómo genera el Arduino una señal PWM? Aquí hay un tutorial realmente bueno sobre PWM:
//arduino.cc/en/Tutorial/SecretsOfArduinoPWM
Veremos solo los puntos necesarios para este proyecto.

En el ATMega328P hay tres temporizadores que se pueden usar para generar señales PWM, cada uno de ellos tiene diferentes características que puede usar. Para cada temporizador corresponden dos registros denominados Registros de comparación de salida A / B (OCRnx) que se utilizan para establecer el ciclo de trabajo de la señal.

En cuanto al ADC, hay un preescalador (ver imagen), que ralentiza el reloj principal para tener un control preciso de la frecuencia PWM. El reloj ralentizado se alimenta a un contador que incrementa un registro de temporizador / contador (TCNTn). Este registro se compara continuamente con el OCRnx, cuando son iguales, se envía una señal a un generador de forma de onda que genera un pulso en el pin de salida. Entonces, el truco es establecer el registro OCRnx en algún valor para cambiar el valor medio de la señal.

Si queremos una señal de 5 V (máximo) debemos establecer un ciclo de trabajo del 100% o 255 en el OCRnx (máximo para un número de 8 bits), mientras que si queremos una señal de 0, 5 V debemos establecer un ciclo de trabajo del 10% o un 25 en el OCRnx.

Como el reloj tiene que llenar el registro TCNTn antes de comenzar desde el principio para un nuevo pulso, la frecuencia de salida del PWM es:

f = (reloj principal) / preescaler / (TCNTn máximo)

Por ejemplo, para el temporizador 0 y 2 (8 bits) sin preescalador será: 16 MHz / 256 = 62.5 KHz, mientras que para el temporizador 1 (16 bits) será 16 MHz / 65536 = 244 Hz.

Decidí usar el temporizador número 2 porque
  • El temporizador 0 es utilizado internamente por Arduino IDE para funciones como millis ();
  • El temporizador 1 tiene una frecuencia de salida demasiado lenta porque es un temporizador de 16 bits.

En el ATMega328P hay diferentes tipos de modos de operación de los temporizadores, pero lo que quería era el Fast PWM sin preescalado para obtener la máxima frecuencia de salida posible.

Paso 15: Configuración de PWM

En el boceto, escribí otra función de inicialización que configura todos los parámetros del funcionamiento del temporizador e inicializa un par de pines. nulo initPins (nulo)
{
// ------------------------------------------------ ---------------------
// configuración de TCCR2A
// ------------------------------------------------ ---------------------
// Estos bits controlan el comportamiento del pin de comparación de salida (OC2A). Si uno o
// ambos bits COM2A1: 0 están establecidos, la salida OC2A anula el
// funcionalidad del puerto normal del pin de E / S al que está conectado.
// Sin embargo, tenga en cuenta que el bit de registro de dirección de datos (DDR)
// correspondiente al pin OC2A debe establecerse para habilitar el
// controlador de salida.
// Cuando OC2A está conectado al pin, la función de COM2A1: 0 bits
// depende de la configuración de WGM22: 0 bits.
//
// Modo PWM rápido
// COM2A1 COM2A0
// 0 0 Operación de puerto normal, OC2A desconectado.
// 0 1 WGM22 = 0: Operación de puerto normal, OC0A desconectado.
// WGM22 = 1: alternar OC2A en Comparar coincidencia.
// 1 0 Borrar OC2A en Comparar coincidencia, establecer OC2A en ABAJO
// 1 1 Borrar OC2A en Comparar coincidencia, borrar OC2A en ABAJO
cbi (TCCR2A, COM2A1);
cbi (TCCR2A, COM2A0);
sbi (TCCR2A, COM2B1);
cbi (TCCR2A, COM2B0);

// Combinado con el bit WGM22 encontrado en el registro TCCR2B, estos bits
// controla la secuencia de conteo del contador, la fuente para el máximo
// (TOP) valor de contador y qué tipo de generación de forma de onda se utilizará
// Los modos de operación admitidos por la unidad de temporizador / contador son:
// - Modo normal (contador),
// - Borrar temporizador en modo Comparar coincidencia (CTC),
// - dos tipos de modos de modulación de ancho de pulso (PWM).
//
// Modo WGM22 WGM21 WGM20 Operación TOP
// 0 0 0 0 Normal 0xFF
// 1 0 0 1 PWM 0xFF
// 2 0 1 0 CTC OCRA
// 3 0 1 1 Rápido PWM 0xFF
// 4 1 0 0 Reservado -
// 5 1 0 1 PWM OCRA
// 6 1 1 0 Reservado -
// 7 1 1 1 Rápido PWM OCRA
cbi (TCCR2B, WGM22);
sbi (TCCR2A, WGM21);
sbi (TCCR2A, WGM20);

// ------------------------------------------------ ---------------------
// configuración de TCCR2B
// ------------------------------------------------ ---------------------
// El bit FOC2A solo está activo cuando los bits WGM especifican un no PWM
// modo.
// Sin embargo, para garantizar la compatibilidad con dispositivos futuros, este bit
// debe establecerse en cero cuando se escribe TCCR2B cuando se opera en PWM
// modo. Al escribir uno lógico en el bit FOC2A, un inmediato
// Comparar coincidencia se fuerza en la unidad de generación de forma de onda. El OC2A
// la salida se cambia según su configuración COM2A1: 0 bits. Tenga en cuenta que
// el bit FOC2A se implementa como una luz estroboscópica. Por eso es el valor
// presente en el COM2A1: 0 bits que determina el efecto de la
// comparación forzada.
// Una luz estroboscópica FOC2A no generará ninguna interrupción, ni se borrará
// el temporizador en modo CTC usando OCR2A como TOP.
// El bit FOC2A siempre se lee como cero.
cbi (TCCR2B, FOC2A);
cbi (TCCR2B, FOC2B);

// Los tres bits de selección de reloj seleccionan la fuente de reloj que utilizará
// el temporizador / contador.
// CS22 CS21 CS20 Prescaler
// 0 0 0 Sin fuente de reloj (Temporizador / Contador detenido).
// 0 0 1 Sin preescalado
// 0 1 0 8
// 0 1 1 32
// 1 0 0 64
// 1 0 1 128
// 1 1 0 256
// 1 1 1 1024
cbi (TCCR2B, CS22);
cbi (TCCR2B, CS21);
sbi (TCCR2B, CS20);

pinMode (errorPin, OUTPUT);
pinMode (umbralPin, SALIDA);

analogWrite (umbralPin, 127);
}

Paso 16: variables volátiles

No recuerdo dónde, pero leí que las variables que se modifican dentro de un ISR deberían declararse como volátiles .

Las variables volátiles son variables que pueden cambiar durante el tiempo, incluso si el programa que se está ejecutando no las modifica. Al igual que Arduino registra que puede cambiar el valor de algunas intervenciones externas.

¿Por qué el compilador quiere saber acerca de tales variables? Esto se debe a que el compilador siempre intenta optimizar el código que escribimos, para hacerlo más rápido, y lo modifica un poco, tratando de no cambiar su significado. Si una variable cambia por sí sola, podría parecerle al compilador que nunca se modifica durante la ejecución de, por ejemplo, un bucle y podría ignorarla; mientras que podría ser crucial que la variable cambie su valor. Al declarar variables volátiles, evita que el compilador modifique el código relacionado con ellas.

Para obtener más información, sugiero leer la página de Wikipedia: //en.wikipedia.org/wiki/Volatile_variable

Paso 17: escribir el núcleo del bosquejo

¡Finalmente hemos llegado al núcleo del programa!

Como vimos antes, quería una adquisición continua y escribí la rutina de servicio de interrupción de ADC para almacenar en el búfer circular los datos continuamente. Se detiene cada vez que alcanza el índice que es igual a stopIndex. El buffer se implementa como circular empleando el operador de módulo.

// ------------------------------------------------ -----------------------------
// Interrupción completa de conversión de ADC
// ------------------------------------------------ -----------------------------
ISR (ADC_vect)
{
// Cuando se lee ADCL, el registro de datos ADC no se actualiza hasta ADCH
// es leído. En consecuencia, si el resultado se deja ajustado y no más
// se requiere una precisión de 8 bits, es suficiente leer ADCH.
// De lo contrario, ADCL debe leerse primero, luego ADCH.
ADCBuffer [ADCCounter] = ADCH;

ADCCounter = (ADCCounter + 1)% ADCBUFFERSIZE;

si (espera)
{
if (stopIndex == ADCCounter)
{
// Situación de congelación
// Deshabilitar ADC y detener el modo de conversión de ejecución libre
cbi (ADCSRA, ADEN);

congelar = verdadero;
}
}
}

La rutina de servicio de interrupción del comparador analógico (que se llama cuando una señal pasa el umbral) se desactiva y le dice al ADC ISR que comience la fase de espera y establece el índice de detención.

// ------------------------------------------------ -----------------------------
// Interrupción del comparador analógico
// ------------------------------------------------ -----------------------------
ISR (ANALOG_COMP_vect)
{
// Deshabilita la interrupción del comparador analógico
cbi (ACSR, ACIE);

// Activar errorPin
// digitalWrite (errorPin, HIGH);
sbi (PORTB, PORTB5);

espera = verdadero;
stopIndex = (ADCCounter + waitDuration)% ADCBUFFERSIZE;
}


¡Esto fue realmente fácil después de toda esa conexión a tierra!

Paso 18: formando la señal entrante

Vayamos al hardware ahora. El circuito puede parecer complicado pero es realmente simple.
  • Hay una resistencia de 1 MΩ en la entrada, para dar una referencia de masa a la señal y tener una entrada de alta impedancia. Una alta impedancia "simula" un circuito abierto si lo conecta a una de menor impedancia, por lo que la presencia del Girino no afecta demasiado al circuito que desea medir.
  • Después de la resistencia hay un seguidor de emisor para desacoplar la señal y proteger la siguiente electrónica.
  • Hay un desplazamiento simple que genera un nivel de 2.5 V con un divisor de voltaje. Está conectado a un condensador para estabilizarlo.
  • Hay un amplificador sumador no inversor que suma la señal entrante y el desplazamiento. Utilicé esta técnica porque quería poder ver también señales negativas, ya que el ADC Arduino podía ver señales solo entre 0 V y 5 V.
  • Después del sum-amp hay otro seguidor de emisor.
  • Un puente nos permite decidir si queremos alimentar la señal con un desplazamiento o no.
El amplificador operacional que pretendía usar era un LM324 que puede trabajar entre 0 V y 5 V, pero también entre, por ejemplo, -12 V a 12 V. Esto nos brinda más posibilidades con las fuentes de alimentación. También probé un TL084 que es mucho más rápido que el LM324 pero requiere una fuente de alimentación dual. Ambos tienen el mismo pinout, por lo que se pueden cambiar sin ninguna modificación del circuito.

Paso 19: condensadores de derivación

Los condensadores de derivación son condensadores que se utilizan para filtrar las fuentes de alimentación de los circuitos integrados (IC) y deben colocarse lo más cerca posible de los pines de alimentación del IC. Se usan generalmente en pareja, una cerámica y una electrolítica porque pueden filtrar diferentes frecuencias.

Paso 20: fuentes de energía

Utilicé una fuente de alimentación dual para el TL084 que se puede convertir en una sola fuente de alimentación para el LM324.

En la imagen podemos ver que usé un par de reguladores de voltaje, un 7812, para +12 V, y un 7912, para -12 V. Los condensadores, como de costumbre, se usan para estabilizar los niveles y sus valores son los sugeridos. en las hojas de datos.

Obviamente para tener un ± 12 V tenemos que tener al menos aproximadamente 30 V en la entrada porque los reguladores de voltaje requieren una entrada más alta para proporcionar una salida estabilizada. Como no tenía esa fuente de alimentación, utilicé el truco de usar dos fuentes de alimentación de 15 V en serie. Uno de los dos está conectado al conector de alimentación Arduino (por lo que alimenta tanto el Arduino como mi circuito) y el otro directamente al circuito.

¡No es un error conectar los +15 V de la segunda fuente de alimentación a la GND de la primera! Así es como obtenemos un -15 V con fuentes de alimentación aisladas .

Si no quiero llevar un Arduino y dos fuentes de alimentación, aún puedo usar los +5 V proporcionados por el Arduino cambiando esos puentes (y usando el LM324).

Paso 21: preparación de un conector blindado

Siempre me han molestado los conectores que pude encontrar para hacer un escudo Arduino, porque siempre tienen pines que son demasiado cortos y las placas que uso solo pueden soldarse en un lado. Así que inventé un pequeño truco para alargar los pasadores y poder soldarlos e insertarlos en el Arduino.

Al insertar la tira de alfileres en el tablero, como en la imagen, podemos empujar los alfileres, para tenerlos solo en un lado del plástico negro. Luego podemos soldarlos en el mismo lado donde se insertarán en el Arduino.

Paso 22: soldadura y prueba

No puedo mostrarle todo el procedimiento de soldadura del circuito porque se sometió a muchos trabajos de prueba y error. Al final se puso un poco desordenado pero no demasiado malo, aunque no mostraré la parte inferior porque eso es realmente desordenado.

En esta etapa no hay mucho que decir porque ya expliqué en detalle todas las partes del circuito. Lo probé con un osciloscopio, que me prestó un amigo, para ver las señales en cada punto del circuito. Parece que todo está funcionando bien y estoy bastante satisfecho.

El conector para la señal entrante puede parecer un poco extraño para alguien que no proviene de High Energy Physics, es un conector LEMO. Es el conector estándar para señales nucleares, al menos en Europa como en los EE. UU. He visto principalmente conectores BNC.

Paso 23: Señales de prueba

Para probar el circuito y la adquisición de datos (DAQ) utilicé un segundo Arduino con un boceto simple que genera pulsos cuadrados con diferentes longitudes. También escribí un script de Python que habla con Girino y le dice que adquiera algunas series de datos y guarda una de ellas en un archivo.
Ambos están apegados a este paso.

Archivos adjuntos

  • TaraturaTempi.ino Descargar
  • readgirino.py Descargar

Paso 24: Calibración de tiempo

Usando las señales de prueba, calibré la escala horizontal de las parcelas. Al medir los anchos de los pulsos (que se conocen porque se generaron) y al trazar los anchos de los pulsos medidos con respecto a los valores conocidos, se obtiene un gráfico lineal. Al hacer esto para cada ajuste de preescalador, tenemos la calibración de tiempo para todas las tasas de adquisición.

En las imágenes podemos ver todos los datos que tomé y analicé. El gráfico "Pendientes ajustadas" es el más interesante porque nos dice la tasa de adquisición real de mi sistema en cada configuración de preescalador. Las pendientes se midieron como un número [ch / ms] pero esto es equivalente a un [kHz], por lo que los valores de las pendientes son en realidad kHz o también kS / s (kilo muestras por segundo). Eso significa que con el preescalador establecido en 8 obtenemos una tasa de adquisición de:

(154 ± 2) kS / s

No está mal, ¿eh?

Mientras que de la gráfica "Intercepciones en Y ajustadas" obtenemos una idea de la linealidad del sistema. Todas las intersecciones en y deben ser cero porque a una señal con longitud cero debe corresponder un pulso con una longitud cero. Como podemos ver en el gráfico, todos son compatibles con cero, pero no con el conjunto de datos de 18 preescaladores. Sin embargo, este conjunto de datos es el peor porque tiene solo dos datos y no se puede confiar en su calibración.

A continuación hay una tabla con las tasas de adquisición para cada configuración de preescaler.

PreescaladorTasa de adquisición [kS / s]
1289, 74 ± 0, 04
6419, 39 ± 0, 06
3237, 3 ± 0, 6
dieciséis75, 5 ± 0, 3
8153 ± 2
Los errores citados provienen del motor de ajuste Gnuplot y no estoy seguro de ellos.

También probé un ajuste no ponderado de las tasas porque puedes ver que se duplican aproximadamente cuando las mitades de preescalado, esto parece una ley de proporcionalidad inversa. Así que ajusté las tasas frente a la configuración del preescalador con una ley simple de

y = a / x

Tengo un valor para un de

a = 1223

con un χ² = 3.14 y 4 grados de libertad, esto significa que la ley se acepta con un nivel de confianza del 95%.

Paso 25: ¡Listo! (Casi)

Al final de esta larga experiencia, me siento muy satisfecho porque
  • Aprendí mucho sobre microcontroladores en general;
  • Aprendí mucho más sobre el Arduino ATMega328P;
  • Tuve una experiencia práctica de adquisición de datos, no usando algo ya hecho sino haciendo algo;
  • Me di cuenta de un osciloscopio aficionado que no es tan malo.
Espero que esta guía sea útil para cualquiera que la lea. Quería escribirlo de manera detallada porque aprendí todo eso de la manera más difícil (navegar por Internet, leer la hoja de datos y con mucha prueba y error) y me gustaría evitar que alguien tenga esa experiencia.

Paso 26: Continuará ...

Sin embargo, el proyecto está lejos de completarse. Lo que falta es:
  1. Una prueba con diferentes señales analógicas (me falta un generador de señales analógicas);
  2. Una interfaz gráfica de usuario para el lado de la computadora.
Mientras que para el punto 1. No estoy seguro de cuándo se completará, porque no estoy planeando comprar / construir uno en el futuro cercano.

Para el punto 2. la situación podría ser mejor. ¿Alguien está dispuesto a ayudarme con eso? Encontré un bonito osciloscopio de Python aquí:
//www.phy.uct.ac.za/courses/python/examples/moreexamples.html#oscilloscope-and-spectrum-analyser
Me gustaría modificarlo para que se ajuste a Girino, pero acepto sugerencias.

Artículos Relacionados