Implementación de un generador de funciones arbitrarias con FPGA Matias Risaro y Marcelo Luda 22 de diciembre de 2014 Resumen En el presente trabajo se detalla la construcción de un generador de funciones arbitrarias digital de dos canales, utilizando una placa FPGA y electrónica de conversión digital-analógica. Se desarrolló una interfaz serie para definir la frecuencia de trabajo y cargar los datos de las funciones arbitrarias. Además el dispositivo construido puede sincronizarse a una función TTL de referencia a través de un PLL digital diseñado. Se incluye el diseño de la electrónica de conversión, la descripción en bloques del diseño FPGA y los ASM de los algoritmos principales implementados. 1. Introducción Un generador de funciones es un dispositivo que permite generar señales de tensión que varı́an de forma periódica en función del tiempo. Es un elemento básico de laboratorios de fı́sica y de varias ingenierı́as en general. En particular, la generación de funciones arbitrarias permite extender sus aplicaciones. Además, poder sincronzar una señal generada a una señal de referencia permite implementar técnicas de medición de alta precisión, como el lock-in, o hacer análisis espectrales de otras señales. En el siguiente informe se detalla el desarrollo de un generador de funciones arbitrarias con resolución en tensión de 8 bits y en tiempo de 6 bits, con una frecuencia máxima de f = 156kHz. Este dispositivo se implementa utilizando una placa de desarrollo Nexys-3 y un DAC AD7545 de 12 bits. El diseño permite controlar el generador desde un ordenador a través de una interfaz UART, pudiendo establecer la forma de la función de onda para dos canales diferentes y la frecuencia de operación. Dicha frecuencia también puede ser definida con una señal externa de referencia de tipo TTL. Para ello se implementó un módulo Phase Locked Loop (PLL) de detección de frecuencia y fase. 1.1. Generador de Funciones Un generador de funciones es un dispositivo capaz generar señales eléctricas periódicas en el tiempo. Son utilizados generalmente para probar otros dispositivos electrónicos y para realizar análisis espectrales. Las funciones de onda más utilizadas son las ondas senoidales, las ondas cuadradas y las triangulares. Estas funciones se pueden construir utilizando elementos totalmente analógicos a partir de un oscilador electrónico de referencia, elementos que componen los generadores de funciones más básicos del mercado. Los generadores de funciones más sofisticados sintetizan la función de onda mediante procesamiento de señales digitales (DSP) y luego utilizan un conversor digital analógico (DAC) para producir una señal analógica. Esto permite ampliar la variedad de formas de onda y se los conoce como AWG (Arbitrary Waveform Generator ) Un DAC es un dispositivo electrónico capaz de convertir un dato de señal digital en señales de tensión analógica. Dichos dispositivos son alimentados con una tensión de referencia, Vcc (tı́picamente 5 V), y generan una tensión continua proporcional al dato que se le ingresa. Un DAC de 7 bits alimentado con 5 V, por ejemplo, discretiza en 128 valores las tensiones entre 0 y 5 Volts. Si se carga a la entrada el número 0100000, que corresponde al número decimal 64, a la salida se obtiene una tensión continua de 2,5 V. El DAC que se utiliza en el presente trabajo es el AD7545, 1 que tiene una resolución de 12 bits y un tiempo de respuesta de aproximadamente 100 ns. La carga de los datos se realiza en forma paralela, a través de las 12 entradas que posee. En la figura 1 se muestra un esquema de las conexiones del AD7545. RFB 20 AD7545 VREF 19 R 12-BIT MULTIPLYING DAC 1 OUT 1 2 AGND 12 WR 17 CS 16 18 VDD INPUT DATA LATCHES 3 DGND 12 DB11–DB0 (PINS 4–15) Figura 1: Esquema de E/S del DAC AD7545 La resolución del AD7545 es de 12 bits, pero no se utiliza a su máxima resolución. Se conectan a tierra los 4 bits menos significativos y se tiene entonces un DAC de 8 bits, que nos permite una resolución en tensiones de aproximadamente 2 mV. Esto es lo que se conoce como resolución vertical de un generador de funciones. Otra de las caracterı́sticas de un generador de funciones es la longitud de la forma de onda, que es la cantidad de puntos que definen la función a repetir periódicamente. En nuestro caso se opta por una longitud de 6 bits (64 puntos) para definir la función de onda. Cabe destacar que con esta elección, y considerando el tiempo de respuesta del DAC, se obtiene el mı́nimo perı́odo que puede tener la función de onda. Dicho perı́odo es T = 100ns ∗ 64 = 6, 4µs, por lo tanto la máxima frecuencia a la que puede trabajar el generador de funciones es f = 1/T = 156kHz. 1.2. PLL La sigla PLL (Phase-Locked Loop) se refiere a técnica de control que permite sincronizar la frecuencia y la fase de una señal generada por un oscilador controlable, a la de una señal de referencia. Dicha técnica representa un elemento básico para múltiples aplicaciones en procesamiento de señales, entre los que se halla la demodulación de una señal de radio FM o la medición de señales débiles por la técnica de Lock-in. La técnica Lock-in permite hacer una análisis espectral de la respuesta de un sistema ante una excitación periódica. Para ello se utiliza una señal de referencia con la misma frecuencia y fase que la señal excitadora, normalmente generada bajo la norma TTL (onda cuadrada de 0 a 5V). La técnica de lock-in consiste en generar funciones de seno y coseno sincornizadas a la señal de referencia con un PLL, multiplicarlas por la señal de respuesta del sistema y filtrarlas con un filtro pasa bajos. Con este procedimiento se obtiene una tensión de salida, correspondiente a los coeficientes de la serie de Fourier de la señal de respuesta del sistema. Un PLL analógico consiste en un detector de fase con un filtro pasa bajos y un VCO (Voltage Controlled Oscillator ) vinculados en forma realimentada como muestra la figura 2. La salida del filtro es una tensión de error, que da cuenta de la diferencia entre la frecuencia de referencia y la del VCO. En este trabajo se implementó un PLL digital que puede lockearse a señales TTL haciendo detección de flancos e induciendo la frecuencia de la señal de referencia. 2 Figura 2: Esquema de un PLL analógico 2. Implementación en FPGA El dispositivo fue desarrollado sobre una placa FPGA de desarrollo Nexys-3 programada en Verilog. A continuación se realiza un análisis de la estructura modular de esta implementación. En la figura 3 se detallan las entradas y salidas del dispositivo y los periféricos asociados. Se incluye una interfaz de comunicación con el ordenador (pines tx y rx), la señal de entrada signal para la referencia, los bus de datos JC y JD para comunicación con los DAC, algunas salidas para el control del display de siete segmentos (an y seg) y algunas entradas para el control de variables internas (NTau, PC_o_PLL y sw_mem). Se incluye una salida extra jb para debugging que sirve para usar de trigger externo en el osciloscopio al visualizar las señales de salida de los DAC. main rx tx JC[8] JD[8] jb Ntau[2] PC_o_PLL sw_mem signal an[4] seg[8] Figura 3: Módulo principal del proyecto Las principales partes del diagrama de bloques interno del generador de funciones se muestran en la figura 4. Cada una de las salidas que controla un DAC está asociada a un módulo de memoria (memoria A y memoria B). Dichos módulos consisten en un banco de registros con un puntero de lectura que avanza cı́clicamente y de a un paso, cada vez que llega un 1’b1 al pin change_val. Además cuenta con una interfaz de escritura compuesta por las entradas mem_reset, mem_wr y mem_wr_dat. El módulo divisor_tau es un divisor de frecuencia programable, implementado con un contador de ciclos de reloj (10ns) y se reinicia cada vez que llega al valor freq emitiendo un tick en la salida. Este módulo alimenta las entradas change_val que hacen avanzar el puntero de lectura de las memorias. El valor de freq es seleccionado entre cfreq o dfreq, dependiendo del valor del switch PC_o_PLL. En el primer estado la frecuencia se define con la interfaz UART, mientras que en el otro se calcula la frecuencia de la señal de referencia con el módulo PLL. Es un valor de 32bit y los 16 bits más significativos se muestran en valor hexadecimal utilizando el display de siete segmentos. 3 rx tx UART freq[32] phase[32] selector_mem mem_reset mem_wr mem_wr_dat[8] memoria A mem_reset mem_wr mem_wr_dat[8] sw_mem change_val rd_reset cfreq[32] selector freq[32] divisor_tau freq[32] tick rd_val[8] memoria B mem_reset mem_wr mem_wr_dat[8] dfreq[32] PC_o_PLL change_val rd_reset [31:28] [27:24] [23:20] [19:16] rd_val[8] disp_mux hex2seg hex2seg hex2seg an[4] seg[8] hex2seg Figura 4: Estructura de módulos del generador de funciones El módulo UART, además de implementar la comunicación con el ordenador incluye la interfaz de escritura de las memorias. El switch sw_mem determina qué memoria se esta programando en un instante dado. También posee dos registros internos de 32 bits para guardar la información de cfreq y phase que se le envı́a desde el ordenador. 2.1. Banco de memoria para funciones arbitrarias El módulo de memoria (figura 5) consiste en un banco de 64 registros de 8 bits. La única salida es rd_val que muestra el valor apuntado por el puntero rd_ptr en cada momento. En cada flanco del reloj en que el pin change_val está en 1’b1 el puntero rd_ptr se incrementa en +1 y al llegar a 64 vuelve a comenzar desde cero. De este modo, cada 64 ticks que llegan a change_val ocurre un periodo completo de la función de onda guardada en la memoria. memoria mem_reset wr_ptr mem_wr mem_wr_dat[8] change_val rd_reset rd_ptr rd_val[8] Figura 5: Módulo de memoria Para llenar el contenido de la memoria se utiliza la interfaz de escritura mencionada en la sección anterior. Cada vez que llega un tick a mem_wr se escribe el valor de mem_wr_dat en el registro apuntado por wr_ptr y se incrementa wr_ptr en +1. Cuando wr_ptr llega a 64 se deshabilita la escritura hasta que este vuelva a ser cero. El pin mem_reset sirve para reiniciar el puntero wr_ptr a cero y habilitar nuevamente la escritura del banco completo. 4 2.2. Módulo UART modificado Sobre la base del módulo UART proporcionado en el curso “Diseño de Sistemas con FPGA” DC-FCEN-UBA1 se programó un módulo de control que interpreta los caracteres enviados desde un ordenador usando el protocolo RS-232. En la figura 6 se puede apreciar el diseño interno del módulo modificado. UART rx stick baud_rate_gen uart_rx uart_cpu fifo_wr din fifo_wr_dat[8] rx_tick fifo_reset dout[8] rx_done stick fifo_empy fifo_full din[8] tx_start tx_done r_data[8] empty rd msg_dat[8] msg_tick stick tx fifo_tx fifo_rd fifo_rd_dat[8] w_data[8] wr uart_tx s_exito s_fallo fifo_rx wr wr_dat[8] fifo_reset empty full rd rd_dat[8] mem_wr mem_wr_dat[8] mem_reset s_exito s_fallo freq[32] phase[32] err_msg Figura 6: Módulo UART modificado para implementar el protocolo de control del dispositivo desde un ordenador El módulo uart_cpu es una FSM que procesa los caracteres que van llegando por rx e implementa diferentes algoritmos en función de ello. El principal se inicia con la llegada de un caracter ’m’ cuando el FSM está en estado idle. En ese caso, se reinicia el fifo_rx (con un tick en el pin fifo_reset) y los siguientes caracteres que lleguen se van a escribir directamente en el el fifo hasta que esté lleno (se active el pin fifo_full). El fifo tiene el mismo tamaño que los módulos de memoria, por lo que se está cargando un periodo completo de la función de onda a producir. Una vez lleno, el uart_cpu reinicia el módulo de memoria (con un tick en el pin mem_reset), lee de a uno los registros del fifo y los escribe en el módulo de memoria utilizando la interfase de escritura ya descrita. Terminado el proceso de escritura, si no hubo errores, envı́a un tick por s_exito que permite enviar un caracter de confirmación a través del tx al ordenador. En caso de que la FSM en estado idle reciba un caracter ’f’ o ’p’ en el bus din, el módulo uart_cpu tomará los siguientes 4 bytes que lleguen y los guardará en el registro freq o phase respectivamente. De este modo quedan cargados dos registros de 32 bits que sirven para fijar la frecuencia de trabajo del generador de funciones (en el caso que la frecuencia se controla desde el ordenador) y un valor que sirve para establecer una diferencia de fase entre la señal de referencia y la generada (en el caso donde se controla la frecuencia y la fase desde el PLL). En la figura 7 se puede ver el diagrama ASM del FSM implementado por uart_cpu. No se incluye la rama de la escritura del valor de phase porque es equivalente al de freq. 1 http://www.dc.uba.ar/materias/disfpga/2014/c2/descargas/UART.rar/view 5 ~('m'|'p'|'f') idle fifo_reset ← 0 fifo_wr ← 0 fif_rd ← 0 mem_reset ← 0 mem_wr ← 0 s_exito ← 0 'p' 'f' din ... reset_fifo_m reset_mem_f 'm' w_en_freq_next ← 0 fifo_reset ← 1 guardar1_f carga_fifo_m w_ptr_freq_next ← 2'b00 fifo_reset ← 0 fifo_wr ← rx_tick w_en_freq_next ← 0 F rx_tick == 1 F T fifo_full == 1 w_en_freq_next ← 1 T reset_mem_m guardar2_f mem_reset ← 1 w_ptr_freq_next ← 2'b01 carga_mem_m w_en_freq_next ← 0 fifo_rd ← 1 mem_wr ← ~fifo_empty mem_reset ← 0 rx_tick == 1 F T F w_en_freq_next ← 1 fifo_empty == 1 exito_m guardar3_f w_ptr_freq_next ← 2'b10 T w_en_freq_next ← 0 s_exito ← 1 fifo_rd ← 0 mem_wr ← 0 rx_tick == 1 F T w_en_freq_next ← 1 guardar4_f w_ptr_freq_next ← 2'b11 w_en_freq_next ← 0 rx_tick == 1 F T w_en_freq_next ← 1 exito_f Figura 7: ASM del módulo 6 2.3. Sincronización en fase y frecuencia Cuando el switch PC_o_PLL está en 1’b1 el generador de funciones trabaja en una frecuencia calculada a partir de la señal de referencia y con su fase lockeada a la esta señal. Para ello se implementaron los tres módulos que se muestran en la figura 8. flank_detector signal fup_tick fdw_tick freq_detector Ntau[2] freq[32] ftick dfreq[32] desfasador ftick func_reset PC_o_PLL func_reset phase[32] freq[32] Figura 8: Módulos que implementan el PLL El módulo flank_detector es una máquina de estados con un flip-flop que detecta cambios en el valor de signal y envı́a un tick por fup_tick cada vez que hay un flanco de subida y un tick por fdw_tick cada vez que hay un flanco de bajada. Con una de estas dos salidas se alimentan los módulos de freq_detector y desfasador. El módulo freq_detector es un contador de ciclos de reloj que se reinicia cada vez que llega un tick a ftick. Antes de reiniciarse guarda el último valor, que representa el número de ticks de reloj que entran en un perı́odo de la señal. Debido a que este número puede variar levemente entre medición y medición (por ruidos, jitter en la señal de referencia u otras fuentes de imprecisión) se utilizó un algoritmo de integración tipo “filtro pasa bajos” para actualizar el valor de freq. En lugar de guardar directamente el valor q_reg relevado en cada medición de flanco se guarda el valor resultante de la fórmula (1). q reg N + freq (256 − N ) (1) 256 Con los switchs NTau se puede elegir que el valor N = 128, 64, 32, 16 según se quiera converger en pocos pasos a la frecuencia de relevada o en muchos pasos con una estabilidad mayor. freq next = 2.4. Electrónica asociada El dispositivo se completa con el desarrollo de la electrónica de conversión digital-analógica de la señal de salida. Para ello se utilizó el DAC integrado AD7545, que funciona como una resistencia programable, y un amplificador operacional (LF357) de salida para la adaptación de impedancias. La elección del DAC radica en la disponibilidad de control por un bus directo de 12 cables, lo que evita la implementación de algún protocolo de control como el I2C o el SPI. El LF357 fue elegido en función de su gran ancho de banda, que permite operar el DAC a la máxima velocidad posible (100 ns). En la figura 9a se puede ver el esquema del circuito de conversión para un canal. En la figura 9b se incluyó el diseño del circuito impreso utilizando dos canales de salida. El circuito fue impreso en una placa PCB y se soldaron todos sus componentes para hacer las pruebas de generación de funciones. Se utilizó una fuente partida de ±6V implementada a partir de una fuente de switching simple y un circuito a medida armado en una protoboard. Se armaron los cables a medida para conectar la FPGA. Se puede ver una foto del dispositivo terminado en la figura 10. 7 8 7 6 5 V+ OUT C1 X1 X1-1 X1-2 1 V+ 22-23-2031 X3-1 X3-2 X3-3 12 DB11 11 DB10 10 DB9 9 DB8 8 DB7 7 DB6 6 DB5 5 DB4 4 DB3 3 DB2 2 DB1 1 DB0 R2 200 1 AD7545_BIS P19 C2 A 2k R3 33p E X2LF357_BIS S V+ GND SV1 22-23-2021 GND 3 R3 AD7545 1 2 3 4 1 22-23-2031 33p LF357 OUT1 GND V- X3 P19 V+ GND GND 33p C1 R1 20 19 18 17 16 15 14 13 12 11 22-23-2021 DB11 DB10 DB9 DB8 DB7 DB6 DB5 AD7545 DB4 DB3 DB2 DB1 DB0 1 2 3 4 5 6 7 8 9 10 OUT1 GND GND 3 R1 200 LF357 22-23-2021 2k 200 12 2k 3 3 R4 SV1 12 SV2 1 V- (a) Esquema para un canal (b) Diseño impreso Figura 9: Circuito de conversión digital-analógico para dos canales Figura 10: Foto del circuito terminado que implementa los dos DAC 3. Caracterización y resultados Para controlar el dispositivo se desarrolló un script en Python donde se definieron las funciones matemáticas que el generador de funciones produce y las funciones necesarias para la comunicación. En el apéndice A se puede ver el código, disponible para ejecutar desde una consola IPhython. En la figura 11 se pueden ver la función senoidal generada por el dispositivo relevada por medio de un osciloscopio. Para bajas frecuencias la resolución temporal del osciloscopio permite ver los pasos de discretización. En la figura 12 se pueden apreciar diferentes funciones arbitrarias generadas con el dispositivo. 8 Generador de funciones, Senoidal Generador de funciones, Senoidal 1 0 0 −1 −1 Tensió n (V) Tensió n (V) 1 −2 −3 −2 −3 −4 −4 −5 −5 −6 −10 −8 −6 −4 −2 0 Tiempo (ms) 2 4 6 8 10 −6 −100 −80 −60 −40 −20 (a) Baja frecuencia 0 Tiempo (us) 20 40 60 80 100 (b) Alta frecuencia Figura 11: Generación de función senoidal a dos frecuencias diferentes. Generador de funciones, Gauss Generador de funciones, Rampa 1 0 0 −1 −1 Tensión (V) Tensión (V) −2 −2 −3 −3 −4 −4 −5 −5 −6 −10 −8 −6 −4 −2 0 Tiempo (ms) 2 4 6 8 −6 −25 10 −20 −15 −10 −5 0 Tiempo (ms) 5 10 15 20 25 (b) Rampa (a) Gaussiana Generador de funciones, Triangular 1 0 Tensión (V) −1 −2 −3 −4 −5 −6 −25 −20 −15 −10 −5 0 Tiempo (ms) 5 10 15 20 25 (c) Triangular Figura 12: Generación de funciones arbitrarias Para caracterizar la precisión en la generación de la funciones se midió la dispersión en frecuencia midiendo el jitter en el periodo de las funciones generadas. Para funciones senoidales la dispersión en diferentes frecuencias se puede ver en la tabla 1. Se incluyen otras funciones arbitrarias generadas en la tabla. Se puede ver que la dispersión hallada es del orden del ∼ 1/1000. 9 Función Seno lento Seno medio Seno rápido Gaussiana Triangular Rampa Frecuencia 156.1 Hz 15.4 KHz 141.6 KHz 15.4 KHz 156.3 KHz 156.3 KHz dispersión 0.2 Hz 0.1 KHz 0.1 KHz 0.1 KHz 0.6 KHz 0.2 KHz Tabla 1: Dispersiones en frecuencia para diferentes frecuencias y funciones generadas Se pudo corroborar el funcionamiento del PLL sincronizando cada una de las funciones arbitrarias generadas, a una señal cuadrada de referencia. La inferencia de la frecuencia de trabajo demostró una velocidad de adaptación muy rápida, con tiempos del orden de 20 ciclos de reloj de la señal de referencia. A modo de ejemplo, se probó la capacidad de generar funciones completamente arbitrarias tratando de reproducir una imagen 2D arbitraria a partir de la generación de las funciones correspondientes a x(t) e y(t) en cada uno de los canales de salida. En la figura 13 se puede ver la pantalla del osciloscopio en modo xy representando una de las imágenes emuladas. Figura 13: Ejemplo práctico de la implementación de funciones arbitrarias en dos canales simultáneos 4. Conclusiones La tecnologı́a FPGA demostró ser perfectamente adecuada para la implementación de un generador de funciones arbitrarias digital. Las frecuencias logradas en este trabajo son suficientemente altas para su utilización en diversas aplicaciones de un laboratorio de fı́sica. Los lı́mites pueden superarse utilizando electrónica de alta velocidad y una placa FPGA de mayores prestaciones. 10 A. Programa de control en Python # Control de generador de funciones en ipython # Triangular fun = abs ( xx -32.) fun = fun - fun . min () fun = fun / fun . max () *255 ftrian = uint8 ( fun ) from numpy import * from matplotlib . pyplot import * import matplotlib . cm as cm import Image # Gaussiana fun = exp ( -(( xx -32.) /8) **2) fun = fun - fun . min () fun = fun / fun . max () *255 fgauss = uint8 ( fun ) # Funcion para convertir un integer en un string de 4 bytes def tau2str ( tau ) : t = uint32 ( tau ) cc = ’ ’ for i in range (0 ,4) : cc = chr ( mod (t ,256) ) + cc t = t /256 return cc # Rampa fun = xx fun = fun - fun . min () fun = fun / float ( fun . max () ) *255 framp = uint8 ( fun ) # Funcion para convertir una tira de numeros uint8 en chars def array2str ( fun ) : ll = ’ ’ for i in fun . tolist () : ll = ll + chr ( i ) return ll # Batman bat_x = array ([ 32 , 52 , 77 , 69 , 72 , 88 , 106 , 112 , 114 , 122 , 131 , 140 , 142 , 146 , 165 , 183 , 180 , 176 , 196 , 214 , 230 , 241 , 250 , 255 , 251 , 244 , 229 , 218 , 205 , 211 , 206 , 193 , 181 , 173 , 162 , 152 , 141 , 133 , 127 , 120 , 115 , 105 , 96 , 86 , 81 , 70 , 57 , 47 , 43 , 46 , 48 , 31 , 20 , 8, 2, 0, 4 , 10 , 17 , 22 , 27 , 31 , 31 , 31] , dtype = uint8 ) # Abrimos el puerto Serie - - - - - - - - - - - - - bat_y = array ([ 41 , 24 , 10 , 38 , 71 , 90 , 71 , 32 , 0 , 26 , 26 , 0 , 23 , 71 , 94 , 65 , 27 , 9 , 23 , 37 , 53 , 71 , 98 , 130 , 159 , 186 , 213 , 231 , 240 , 206 , 180 , 180 , 193 , 207 , 183 , 180 , 202 , 227 , 255 ,227 , 207 , 185 , 177 , 194 , 204 , 190 , 178 , 178 , 198 , 224 , 237 , 223 , 206 , 182 , 157 , 126 , 95 , 77 , 65 , 53 , 49 , 44 , 44 , 44] , dtype = uint8 ) import serial ser = serial . Serial ( port = ’/ dev / ttyUSB2 ’ , baudrate =19200) # Configuramos frecuencia de operacion freq =20000 # x10 ns entre punto y punto cic = int (1/( freq *1 e -8) ) ciclos = tau2str ( cic ) ser . write ( ’ 66 ’. decode ( ’ hex ’) ) ser . write ( ciclos ) # Comandos para comunicacion directa def set_freq ( ser , freq ) : ser . write ( ’f ’+ tau2str ( freq ) ) # Generamos las funciones xx = arange (0 ,64) def set_phase ( ser , phase ) : ser . write ( ’p ’+ tau2str ( phase ) ) # Seno fun = sin ( xx * pi /32) fun = fun - fun . min () fun = fun / fun . max () *255 fsin = uint8 ( fun ) def sext_fun ( ser , fun ) : ser . write ( ’m ’+ array2str ( fun ) ) control generador funciones.py 11